From 8365d3b4742cddd91231c398d3b1f98aff8820be Mon Sep 17 00:00:00 2001 From: "Stephen Lewis (Burrows)" Date: Fri, 26 Jun 2026 13:01:16 -0700 Subject: [PATCH 01/78] Support multiple products and only copy/compile files for those products (#18089) --- mmv1/main.go | 14 +++- mmv1/provider/terraform.go | 38 +++++++-- mmv1/provider/terraform_tgc.go | 80 +++++++++++++------ mmv1/provider/terraform_tgc_cai2hcl.go | 25 +++++- mmv1/provider/terraform_tgc_next.go | 69 +++++++++++++--- ...amework_provider_access_token_test.go.tmpl | 2 + .../framework_provider_scopes_test.go.tmpl | 2 + ...rovider_user_project_override_test.go.tmpl | 2 +- 8 files changed, 182 insertions(+), 50 deletions(-) diff --git a/mmv1/main.go b/mmv1/main.go index b65c7a4374af..d5904b33fdad 100644 --- a/mmv1/main.go +++ b/mmv1/main.go @@ -103,8 +103,9 @@ func GenerateProducts(product, resource, providerName, version, outputPath, base productsToGenerate = append(productsToGenerate, p.PackagePath) } } else { - var productToGenerate = fmt.Sprintf("products/%s", product) - productsToGenerate = []string{productToGenerate} + for _, prod := range strings.Split(product, ",") { + productsToGenerate = append(productsToGenerate, fmt.Sprintf("products/%s", strings.TrimSpace(prod))) + } } for _, productApi := range loadedProducts { @@ -122,8 +123,8 @@ func GenerateProducts(product, resource, providerName, version, outputPath, base }) // In order to only copy/compile files once per provider this must be called outside - // of the products loop. Create an MMv1 provider with an arbitrary product (the first loaded). - providerToGenerate := newProvider(providerName, version, productsForVersion[0], startTime, wrappedFS) + // of the products loop. Create an MMv1 provider with a nil product to trigger shared file behavior. + providerToGenerate := newProvider(providerName, version, nil, startTime, wrappedFS) providerToGenerate.CopyCommonFiles(outputPath, generateCode, generateDocs) if generateCode { @@ -148,6 +149,11 @@ func GenerateProduct(version, providerName string, productApi *api.Product, outp log.Printf("%s: Generating files", productApi.PackagePath) providerToGenerate := newProvider(providerName, version, productApi, startTime, fsys) providerToGenerate.Generate(outputPath, resourceToGenerate, generateCode, generateDocs) + + providerToGenerate.CopyCommonFiles(outputPath, generateCode, generateDocs) + if generateCode { + providerToGenerate.CompileCommonFiles(outputPath, []*api.Product{productApi}, "") + } } func newProvider(providerName, version string, productApi *api.Product, startTime time.Time, fsys fs.FS) provider.Provider { diff --git a/mmv1/provider/terraform.go b/mmv1/provider/terraform.go index 08ae23f035bd..01877b911d82 100644 --- a/mmv1/provider/terraform.go +++ b/mmv1/provider/terraform.go @@ -59,14 +59,16 @@ func NewTerraform(product *api.Product, versionName string, startTime time.Time, IAMResourceCount: 0, Product: product, TargetVersionName: versionName, - Version: *product.VersionObjOrClosest(versionName), StartTime: startTime, templateFS: templateFS, } - t.Product.ImportPath = ImportPathFromVersion(versionName) - for _, r := range t.Product.Objects { - r.ImportPath = t.Product.ImportPath + if product != nil { + t.Version = *product.VersionObjOrClosest(versionName) + t.Product.ImportPath = ImportPathFromVersion(versionName) + for _, r := range t.Product.Objects { + r.ImportPath = t.Product.ImportPath + } } return t @@ -458,6 +460,20 @@ func (t Terraform) getCommonCopyFiles(versionName string, generateCode, generate // key is the target file and value is the source file commonCopyFiles := make(map[string]string, 0) + // Case 0: If we're generating a specific product, only copy files for that product. + if t.Product != nil { + if !generateCode { + return commonCopyFiles + } + googleDir := "google" + if versionName != "ga" { + googleDir = fmt.Sprintf("google-%s", versionName) + } + files := t.getCopyFilesInFolder("third_party/terraform/services/"+t.Product.ApiName, googleDir) + maps.Copy(commonCopyFiles, files) + return commonCopyFiles + } + // Case 1: When copy all of files except .tmpl in a folder to the root directory of downstream repository, // save the folder name to foldersCopiedToRootDir foldersCopiedToRootDir := []string{"third_party/terraform/META.d", "third_party/terraform/version"} @@ -493,7 +509,6 @@ func (t Terraform) getCommonCopyFiles(versionName string, generateCode, generate "third_party/terraform/fwvalidators", "third_party/terraform/provider", "third_party/terraform/registry", - "third_party/terraform/services", "third_party/terraform/sweeper", "third_party/terraform/test-fixtures", "third_party/terraform/tpgdclresource", @@ -596,7 +611,9 @@ func (t Terraform) CopyFileList(outputFolder string, files map[string]string, ge // Compiles files that are shared at the provider level func (t Terraform) CompileCommonFiles(outputFolder string, products []*api.Product, overridePath string) { log.Printf("Generating common files for %s", ProviderName(t)) - t.generateResourcesForVersion(products) + if t.Product == nil { + t.generateResourcesForVersion(products) + } files := t.getCommonCompileFiles(t.TargetVersionName) templateData := NewTemplateData(outputFolder, t.TargetVersionName, t.templateFS) t.CompileFileList(outputFolder, files, *templateData, products) @@ -608,6 +625,14 @@ func (t Terraform) getCommonCompileFiles(versionName string) map[string]string { // key is the target file and the value is the source file commonCompileFiles := make(map[string]string, 0) + if t.Product != nil { + googleDir := "google" + if versionName != "ga" { + googleDir = fmt.Sprintf("google-%s", versionName) + } + return t.getCompileFilesInFolder("third_party/terraform/services/"+t.Product.ApiName, googleDir) + } + // Case 1: When compile all of files except .tmpl in a folder to the root directory of downstream repository, // save the folder name to foldersCopiedToRootDir foldersCompiledToRootDir := []string{"third_party/terraform/scripts"} @@ -628,7 +653,6 @@ func (t Terraform) getCommonCompileFiles(versionName string) map[string]string { "third_party/terraform/fwresource", "third_party/terraform/fwtransport", "third_party/terraform/provider", - "third_party/terraform/services", "third_party/terraform/sweeper", "third_party/terraform/test-fixtures", "third_party/terraform/tpgdclresource", diff --git a/mmv1/provider/terraform_tgc.go b/mmv1/provider/terraform_tgc.go index 8d08f416c882..ec25e4837429 100644 --- a/mmv1/provider/terraform_tgc.go +++ b/mmv1/provider/terraform_tgc.go @@ -58,8 +58,10 @@ func NewTerraformGoogleConversion(product *api.Product, versionName string, star templateFS: templateFS, } - for _, r := range t.Product.Objects { - r.ImportPath = ImportPathFromVersion(versionName) + if product != nil { + for _, r := range t.Product.Objects { + r.ImportPath = ImportPathFromVersion(versionName) + } } return t @@ -185,23 +187,7 @@ func (tgc *TerraformGoogleConversion) generateCaiIamResources(products []*api.Pr func (tgc TerraformGoogleConversion) CompileCommonFiles(outputFolder string, products []*api.Product, overridePath string) { log.Printf("Compiling common files for tgc.") - tgc.generateCaiIamResources(products) - tgc.NonDefinedTests = retrieveFullManifestOfNonDefinedTests(tgc.templateFS) - - files := retrieveFullListOfTestFiles(tgc.templateFS) - for _, file := range files { - tgc.Tests = append(tgc.Tests, strings.Split(file, ".")[0]) - } - tgc.Tests = slices.Compact(tgc.Tests) - - testSource := make(map[string]string) - for target, source := range retrieveTestSourceCodeWithLocation(tgc.templateFS, ".tmpl") { - target := strings.Replace(target, "go.tmpl", "go", 1) - testSource[target] = source - } - templateData := NewTemplateData(outputFolder, tgc.TargetVersionName, tgc.templateFS) - tgc.CompileFileList(outputFolder, testSource, *templateData, products) resourceConverters := map[string]string{ "converters/google/resources/resource_converters.go": "third_party/tgc/resource_converters.go.tmpl", @@ -220,7 +206,40 @@ func (tgc TerraformGoogleConversion) CompileCommonFiles(outputFolder string, pro "converters/google/resources/services/spanner/iam_spanner_database.go": "third_party/terraform/services/spanner/iam_spanner_database.go.tmpl", "converters/google/resources/services/spanner/iam_spanner_instance.go": "third_party/terraform/services/spanner/iam_spanner_instance.go.tmpl", } - tgc.CompileFileList(outputFolder, resourceConverters, *templateData, products) + + filteredFiles := make(map[string]string) + if tgc.Product != nil { + for target, source := range resourceConverters { + if strings.Contains(target, "/services/"+tgc.Product.ApiName+"/") { + filteredFiles[target] = source + } + } + tgc.CompileFileList(outputFolder, filteredFiles, *templateData, products) + } else { + // Shared compilation + tgc.generateCaiIamResources(products) + tgc.NonDefinedTests = retrieveFullManifestOfNonDefinedTests(tgc.templateFS) + + files := retrieveFullListOfTestFiles(tgc.templateFS) + for _, file := range files { + tgc.Tests = append(tgc.Tests, strings.Split(file, ".")[0]) + } + tgc.Tests = slices.Compact(tgc.Tests) + + testSource := make(map[string]string) + for target, source := range retrieveTestSourceCodeWithLocation(tgc.templateFS, ".tmpl") { + target := strings.Replace(target, "go.tmpl", "go", 1) + testSource[target] = source + } + tgc.CompileFileList(outputFolder, testSource, *templateData, products) + + for target, source := range resourceConverters { + if !strings.Contains(target, "/services/") { + filteredFiles[target] = source + } + } + tgc.CompileFileList(outputFolder, filteredFiles, *templateData, products) + } } func (tgc TerraformGoogleConversion) CompileFileList(outputFolder string, files map[string]string, fileTemplate TemplateData, products []*api.Product) { @@ -359,9 +378,6 @@ func (tgc TerraformGoogleConversion) CopyCommonFiles(outputFolder string, genera return } - tgc.CopyFileList(outputFolder, retrieveFullListOfTestTilesWithLocation(tgc.templateFS)) - tgc.CopyFileList(outputFolder, retrieveTestSourceCodeWithLocation(tgc.templateFS, ".go")) - resourceConverters := map[string]string{ "../caiasset/asset.go": "third_party/tgc/caiasset/asset.go", "converters/google/resources/cai/constants.go": "third_party/tgc/cai/constants.go", @@ -456,7 +472,25 @@ func (tgc TerraformGoogleConversion) CopyCommonFiles(outputFolder string, genera "tfplan2cai.go": "third_party/tgc/tfplan2cai.go", "tfplan_to_cai_test.go": "third_party/tgc/tfplan_to_cai_test.go", } - tgc.CopyFileList(outputFolder, resourceConverters) + + filteredFiles := make(map[string]string) + if tgc.Product != nil { + for target, source := range resourceConverters { + if strings.Contains(target, "/services/"+tgc.Product.ApiName+"/") { + filteredFiles[target] = source + } + } + } else { + // Shared files + tgc.CopyFileList(outputFolder, retrieveFullListOfTestTilesWithLocation(tgc.templateFS)) + tgc.CopyFileList(outputFolder, retrieveTestSourceCodeWithLocation(tgc.templateFS, ".go")) + for target, source := range resourceConverters { + if !strings.Contains(target, "/services/") { + filteredFiles[target] = source + } + } + } + tgc.CopyFileList(outputFolder, filteredFiles) } func (tgc TerraformGoogleConversion) CopyFileList(outputFolder string, files map[string]string) { diff --git a/mmv1/provider/terraform_tgc_cai2hcl.go b/mmv1/provider/terraform_tgc_cai2hcl.go index a31220f0de2a..7912466ff7e2 100644 --- a/mmv1/provider/terraform_tgc_cai2hcl.go +++ b/mmv1/provider/terraform_tgc_cai2hcl.go @@ -14,10 +14,13 @@ package provider import ( + "errors" "fmt" "io/fs" "log" "os" + "path/filepath" + "strings" "time" "github.com/GoogleCloudPlatform/magic-modules/mmv1/api" @@ -43,8 +46,10 @@ func NewCaiToTerraformConversion(product *api.Product, versionName string, start templateFS: templateFS, } - for _, r := range t.Product.Objects { - r.ImportPath = ImportPathFromVersion(versionName) + if product != nil { + for _, r := range t.Product.Objects { + r.ImportPath = ImportPathFromVersion(versionName) + } } return t @@ -66,7 +71,19 @@ func (cai2hcl CaiToTerraformConversion) CopyCommonFiles(outputFolder string, gen log.Println(fmt.Errorf("error creating output directory %v: %v", outputFolder, err)) } - if err := copy.Copy("third_party/cai2hcl", outputFolder); err != nil { - log.Println(fmt.Errorf("error copying directory %v: %v", outputFolder, err)) + if cai2hcl.Product != nil { + srcDir := filepath.Join("third_party/cai2hcl/services", cai2hcl.Product.ApiName) + dstDir := filepath.Join(outputFolder, "services", cai2hcl.Product.ApiName) + if err := copy.Copy(srcDir, dstDir); err != nil && !errors.Is(err, os.ErrNotExist) { + log.Println(fmt.Errorf("error copying service directory %v: %v", srcDir, err)) + } + } else { + if err := copy.Copy("third_party/cai2hcl", outputFolder, copy.Options{ + Skip: func(srcinfo os.FileInfo, src, dest string) (bool, error) { + return strings.Contains(src, "/services/"), nil + }, + }); err != nil { + log.Println(fmt.Errorf("error copying directory %v: %v", outputFolder, err)) + } } } diff --git a/mmv1/provider/terraform_tgc_next.go b/mmv1/provider/terraform_tgc_next.go index d4effa4d6e9f..c3181dcec152 100644 --- a/mmv1/provider/terraform_tgc_next.go +++ b/mmv1/provider/terraform_tgc_next.go @@ -75,9 +75,11 @@ func NewTerraformGoogleConversionNext(product *api.Product, versionName string, templateFS: templateFS, } - t.Product.ImportPath = ImportPathFromVersion(versionName) - for _, r := range t.Product.Objects { - r.ImportPath = ImportPathFromVersion(versionName) + if product != nil { + t.Product.ImportPath = ImportPathFromVersion(versionName) + for _, r := range t.Product.Objects { + r.ImportPath = ImportPathFromVersion(versionName) + } } return t @@ -180,8 +182,6 @@ func (tgc *TerraformGoogleConversionNext) GenerateProduct(outputFolder string) { } func (tgc TerraformGoogleConversionNext) CompileCommonFiles(outputFolder string, products []*api.Product, overridePath string) { - tgc.generateResourcesForVersion(products) - resourceConverters := map[string]string{ // common "pkg/transport/config.go": "third_party/terraform/transport/config.go.tmpl", @@ -206,7 +206,25 @@ func (tgc TerraformGoogleConversionNext) CompileCommonFiles(outputFolder string, } templateData := NewTemplateData(outputFolder, tgc.TargetVersionName, tgc.templateFS) - tgc.CompileFileList(outputFolder, resourceConverters, *templateData, products) + filteredFiles := make(map[string]string) + + if tgc.Product != nil { + for target, source := range resourceConverters { + if strings.Contains(target, "/services/"+tgc.Product.ApiName+"/") { + filteredFiles[target] = source + } + } + tgc.CompileFileList(outputFolder, filteredFiles, *templateData, products) + } else { + // Shared compilation + tgc.generateResourcesForVersion(products) + for target, source := range resourceConverters { + if !strings.Contains(target, "/services/") { + filteredFiles[target] = source + } + } + tgc.CompileFileList(outputFolder, filteredFiles, *templateData, products) + } } func (tgc TerraformGoogleConversionNext) CompileFileList(outputFolder string, files map[string]string, fileTemplate TemplateData, products []*api.Product) { @@ -249,10 +267,6 @@ func (tgc TerraformGoogleConversionNext) CopyCommonFiles(outputFolder string, ge log.Println(fmt.Errorf("error creating output directory %v: %v", outputFolder, err)) } - if err := copy.Copy("third_party/tgc_next", outputFolder); err != nil { - log.Println(fmt.Errorf("error copying directory %v: %v", outputFolder, err)) - } - resourceConverters := map[string]string{ // common "pkg/envvar/envvar_utils.go": "third_party/terraform/envvar/envvar_utils.go", @@ -284,7 +298,40 @@ func (tgc TerraformGoogleConversionNext) CopyCommonFiles(outputFolder string, ge "pkg/services/resourcemanagerv3/client.go": "third_party/terraform/services/resourcemanagerv3/client.go", "pkg/services/storage/client.go": "third_party/terraform/services/storage/client.go", } - tgc.CopyFileList(outputFolder, resourceConverters) + + filteredFiles := make(map[string]string) + + if tgc.Product != nil { + // Service-specific copying + for target, source := range resourceConverters { + if strings.Contains(target, "/services/"+tgc.Product.ApiName+"/") { + filteredFiles[target] = source + } + } + tgc.CopyFileList(outputFolder, filteredFiles) + + srcDir := filepath.Join("third_party/tgc_next/pkg/services", tgc.Product.ApiName) + dstDir := filepath.Join(outputFolder, "pkg/services", tgc.Product.ApiName) + if err := copy.Copy(srcDir, dstDir); err != nil && !errors.Is(err, os.ErrNotExist) { + log.Println(fmt.Errorf("error copying service directory %v: %v", srcDir, err)) + } + } else { + // Shared copying + if err := copy.Copy("third_party/tgc_next", outputFolder, copy.Options{ + Skip: func(srcinfo os.FileInfo, src, dest string) (bool, error) { + return strings.Contains(src, "pkg/services/"), nil + }, + }); err != nil { + log.Println(fmt.Errorf("error copying directory %v: %v", outputFolder, err)) + } + + for target, source := range resourceConverters { + if !strings.Contains(target, "/services/") { + filteredFiles[target] = source + } + } + tgc.CopyFileList(outputFolder, filteredFiles) + } } func (tgc TerraformGoogleConversionNext) CopyTfToCaiCommonFiles(outputFolder string) { diff --git a/mmv1/third_party/terraform/fwprovider/framework_provider_access_token_test.go.tmpl b/mmv1/third_party/terraform/fwprovider/framework_provider_access_token_test.go.tmpl index 0764832bd172..439f31d10a4c 100644 --- a/mmv1/third_party/terraform/fwprovider/framework_provider_access_token_test.go.tmpl +++ b/mmv1/third_party/terraform/fwprovider/framework_provider_access_token_test.go.tmpl @@ -10,7 +10,9 @@ import ( "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-provider-google/google/acctest" +{{- if ne $.TargetVersionName "ga" }} _ "github.com/hashicorp/terraform-provider-google/google/services/firebase" +{{- end }} "github.com/hashicorp/terraform-provider-google/google/envvar" ) diff --git a/mmv1/third_party/terraform/fwprovider/framework_provider_scopes_test.go.tmpl b/mmv1/third_party/terraform/fwprovider/framework_provider_scopes_test.go.tmpl index 926152e2f424..f72fb7af673a 100644 --- a/mmv1/third_party/terraform/fwprovider/framework_provider_scopes_test.go.tmpl +++ b/mmv1/third_party/terraform/fwprovider/framework_provider_scopes_test.go.tmpl @@ -9,7 +9,9 @@ import ( "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-provider-google/google/acctest" +{{- if ne $.TargetVersionName "ga" }} _ "github.com/hashicorp/terraform-provider-google/google/services/firebase" +{{- end }} "github.com/hashicorp/terraform-provider-google/google/transport" ) diff --git a/mmv1/third_party/terraform/fwprovider/framework_provider_user_project_override_test.go.tmpl b/mmv1/third_party/terraform/fwprovider/framework_provider_user_project_override_test.go.tmpl index b4fd37a01f03..cd75fd19b869 100644 --- a/mmv1/third_party/terraform/fwprovider/framework_provider_user_project_override_test.go.tmpl +++ b/mmv1/third_party/terraform/fwprovider/framework_provider_user_project_override_test.go.tmpl @@ -9,8 +9,8 @@ import ( "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-provider-google/google/acctest" - _ "github.com/hashicorp/terraform-provider-google/google/services/firebase" {{ if ne $.TargetVersionName `ga` -}} + _ "github.com/hashicorp/terraform-provider-google/google/services/firebase" "github.com/hashicorp/terraform-provider-google/google/envvar" ptu "github.com/hashicorp/terraform-provider-google/google/provider/testutils" transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" From 9a32d06455a83163b0fa50b4ea2b3c3dd6955704 Mon Sep 17 00:00:00 2001 From: Cameron Thornton Date: Fri, 26 Jun 2026 15:50:46 -0500 Subject: [PATCH 02/78] Update breaking changes docs and create version 8 upgrade guide (#18110) --- .../make-a-breaking-change.md | 6 +- .../guides/version_8_upgrade.html.markdown | 93 +++++++++++++++++++ 2 files changed, 96 insertions(+), 3 deletions(-) create mode 100644 mmv1/third_party/terraform/website/docs/guides/version_8_upgrade.html.markdown diff --git a/docs/content/breaking-changes/make-a-breaking-change.md b/docs/content/breaking-changes/make-a-breaking-change.md index f948ab961f24..9e96ca4cd1d8 100644 --- a/docs/content/breaking-changes/make-a-breaking-change.md +++ b/docs/content/breaking-changes/make-a-breaking-change.md @@ -1,6 +1,6 @@ --- -majorVersion: "7.0.0" -upgradeGuide: "version_7_upgrade.html.markdown" +majorVersion: "8.0.0" +upgradeGuide: "version_8_upgrade.html.markdown" title: "Make a breaking change" summary: "Guidance on making a breaking changes" weight: 20 @@ -65,7 +65,7 @@ The general process for contributing a breaking change to the 1. Make the `main` branch forwards-compatible with the major release 2. Add deprecations and warnings to the `main` branch of `magic-modules` -3. Add upgrade guide entries to the `FEATURE-BRANCH-major-release-7.0.0` branch of `magic-modules` +3. Add upgrade guide entries to the `FEATURE-BRANCH-major-release-{{% param "majorVersion" %}}` branch of `magic-modules` 4. Make the breaking change on `FEATURE-BRANCH-major-release-{{% param "majorVersion" %}}` These are covered in more detail in the following sections. The upgrade guide diff --git a/mmv1/third_party/terraform/website/docs/guides/version_8_upgrade.html.markdown b/mmv1/third_party/terraform/website/docs/guides/version_8_upgrade.html.markdown new file mode 100644 index 000000000000..b0e703b007fe --- /dev/null +++ b/mmv1/third_party/terraform/website/docs/guides/version_8_upgrade.html.markdown @@ -0,0 +1,93 @@ +--- +page_title: "Terraform provider for Google Cloud 8.0.0 Upgrade Guide" +description: |- + Terraform provider for Google Cloud 8.0.0 Upgrade Guide +--- + +# Terraform Google Provider 8.0.0 Upgrade Guide + +The `8.0.0` release of the Google provider for Terraform is a major version and +includes some changes that you will need to consider when upgrading. This guide +is intended to help with that process and focuses only on the changes necessary +to upgrade from the final `7.X` series release to `8.0.0`. + +Most of the changes outlined in this guide have been previously marked as +deprecated in the Terraform `plan`/`apply` output throughout previous provider +releases, up to and including the final `7.X` series release. These changes, +such as deprecation notices, can always be found in the CHANGELOG of the +affected providers. [google](https://github.com/hashicorp/terraform-provider-google/blob/main/CHANGELOG.md) +[google-beta](https://github.com/hashicorp/terraform-provider-google-beta/blob/main/CHANGELOG.md) + +## I accidentally upgraded to 8.0.0, how do I downgrade to `7.X`? + +If you've inadvertently upgraded to `8.0.0`, first see the +[Provider Version Configuration Guide](#provider-version-configuration) to lock +your provider version; if you've constrained the provider to a lower version +such as shown in the previous version example in that guide, Terraform will pull +in a `7.X` series release on `terraform init`. + +If you've only ran `terraform init` or `terraform plan`, your state will not +have been modified and downgrading your provider is sufficient. + +If you've ran `terraform refresh` or `terraform apply`, Terraform may have made +state changes in the meantime. + +* If you're using a local state, or a remote state backend that does not support +versioning, `terraform refresh` with a downgraded provider is likely sufficient +to revert your state. The Google provider generally refreshes most state +information from the API, and the properties necessary to do so have been left +unchanged. + +* If you're using a remote state backend that supports versioning such as +[Google Cloud Storage](https://developer.hashicorp.com/terraform/language/settings/backends/gcs), +you can revert the Terraform state file to a previous version. If you do +so and Terraform had created resources as part of a `terraform apply` in the +meantime, you'll need to either delete them by hand or `terraform import` them +so Terraform knows to manage them. + +## Provider Version Configuration + +-> Before upgrading to version 8.0.0, it is recommended to upgrade to the most +recent `7.X` series release of the provider, make the changes noted in this guide, +and ensure that your environment successfully runs +[`terraform plan`](https://developer.hashicorp.com/terraform/cli/commands/plan) +without unexpected changes or deprecation notices. + +It is recommended to use [version constraints](https://developer.hashicorp.com/terraform/language/providers/requirements#requiring-providers) +when configuring Terraform providers. If you are following that recommendation, +update the version constraints in your Terraform configuration and run +[`terraform init`](https://developer.hashicorp.com/terraform/cli/commands/init) to download +the new version. + +If you aren't using version constraints, you can use `terraform init -upgrade` +in order to upgrade your provider to the latest released version. + +For example, given this previous configuration: + +```hcl +terraform { + required_providers { + google = { + version = "~> 7.30.0" + } + } +} +``` + +An updated configuration: + +```hcl +terraform { + required_providers { + google = { + version = "~> 8.0.0" + } + } +} +``` + +## Provider + +## Datasources + +## Resources From 6469167cc4ade51af5592a1a9852d7da23f3ff74 Mon Sep 17 00:00:00 2001 From: Piotr Witkiewicz Date: Fri, 26 Jun 2026 23:25:04 +0200 Subject: [PATCH 03/78] Add ANY_RESERVATION_THEN_FAIL reservation affinity to container module (#18094) --- mmv1/third_party/terraform/go.mod | 2 +- mmv1/third_party/terraform/go.sum | 4 +- .../services/container/node_config.go.tmpl | 2 +- .../resource_container_cluster_test.go.tmpl | 101 ++++++++++++++++++ .../resource_container_node_pool_test.go.tmpl | 81 ++++++++++++++ .../docs/r/container_cluster.html.markdown | 3 +- .../docs/r/container_node_pool.html.markdown | 3 +- 7 files changed, 190 insertions(+), 6 deletions(-) diff --git a/mmv1/third_party/terraform/go.mod b/mmv1/third_party/terraform/go.mod index 75484a9766ef..d64a7d5e3250 100644 --- a/mmv1/third_party/terraform/go.mod +++ b/mmv1/third_party/terraform/go.mod @@ -38,7 +38,7 @@ require ( golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 golang.org/x/net v0.56.0 golang.org/x/oauth2 v0.36.0 - google.golang.org/api v0.285.0 + google.golang.org/api v0.286.0 google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad google.golang.org/grpc v1.81.1 google.golang.org/protobuf v1.36.11 diff --git a/mmv1/third_party/terraform/go.sum b/mmv1/third_party/terraform/go.sum index 6c0b656ffd24..251248a379fc 100644 --- a/mmv1/third_party/terraform/go.sum +++ b/mmv1/third_party/terraform/go.sum @@ -429,8 +429,8 @@ golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhS golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/api v0.285.0 h1:B7eHHoKGAX/LrPkQvhQqnGwjgWxofbdGwCTQvpm8FkM= -google.golang.org/api v0.285.0/go.mod h1:NlOlUIr8MPoIhT9Bb/oUnRuHbJOLwxb6JSYJM8Yz+jQ= +google.golang.org/api v0.286.0 h1:TdTXMvzYKnWV1/lPbCdbXRqBrkDqjPto22H2xeZZ8LI= +google.golang.org/api v0.286.0/go.mod h1:NlOlUIr8MPoIhT9Bb/oUnRuHbJOLwxb6JSYJM8Yz+jQ= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= diff --git a/mmv1/third_party/terraform/services/container/node_config.go.tmpl b/mmv1/third_party/terraform/services/container/node_config.go.tmpl index 38388857606e..2acb535a919a 100644 --- a/mmv1/third_party/terraform/services/container/node_config.go.tmpl +++ b/mmv1/third_party/terraform/services/container/node_config.go.tmpl @@ -601,7 +601,7 @@ func schemaNodeConfig() *schema.Schema { Required: true, ForceNew: true, Description: `Corresponds to the type of reservation consumption.`, - ValidateFunc: validation.StringInSlice([]string{"UNSPECIFIED", "NO_RESERVATION", "ANY_RESERVATION", "SPECIFIC_RESERVATION"}, false), + ValidateFunc: validation.StringInSlice([]string{"UNSPECIFIED", "NO_RESERVATION", "ANY_RESERVATION", "SPECIFIC_RESERVATION", "ANY_RESERVATION_THEN_FAIL"}, false), }, "key": { Type: schema.TypeString, diff --git a/mmv1/third_party/terraform/services/container/resource_container_cluster_test.go.tmpl b/mmv1/third_party/terraform/services/container/resource_container_cluster_test.go.tmpl index 78ad68f5c8ab..02d53f9bce5a 100644 --- a/mmv1/third_party/terraform/services/container/resource_container_cluster_test.go.tmpl +++ b/mmv1/third_party/terraform/services/container/resource_container_cluster_test.go.tmpl @@ -3214,6 +3214,38 @@ func TestAccContainerCluster_withNodeConfigReservationAffinitySpecific(t *testin }) } +func TestAccContainerCluster_withNodeConfigReservationAffinityAnyReservationThenFail(t *testing.T) { + t.Parallel() + + reservationName := fmt.Sprintf("tf-test-reservation-%s", acctest.RandString(t, 10)) + clusterName := fmt.Sprintf("tf-test-cluster-%s", acctest.RandString(t, 10)) + networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") + subnetworkName := tpgcompute.BootstrapSubnet(t, "gke-cluster", networkName) + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccCheckContainerClusterDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccContainerCluster_withNodeConfigReservationAffinityAnyReservationThenFail(reservationName, clusterName, networkName, subnetworkName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_container_cluster.with_node_config", + "node_config.0.reservation_affinity.#", "1"), + resource.TestCheckResourceAttr("google_container_cluster.with_node_config", + "node_config.0.reservation_affinity.0.consume_reservation_type", "ANY_RESERVATION_THEN_FAIL"), + ), + }, + { + ResourceName: "google_container_cluster.with_node_config", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + }, + }) +} + func TestAccContainerCluster_withNodeConfigNodeImageConfig(t *testing.T) { t.Parallel() @@ -10472,6 +10504,75 @@ resource "google_container_cluster" "with_node_config" { `, reservation, clusterName, networkName, subnetworkName) } +func testAccContainerCluster_withNodeConfigReservationAffinityAnyReservationThenFail(reservation, clusterName, networkName, subnetworkName string) string { + return fmt.Sprintf(` + +resource "google_project_service" "compute" { + service = "compute.googleapis.com" +} + +resource "google_project_service" "container" { + service = "container.googleapis.com" + depends_on = [google_project_service.compute] +} + + +resource "google_compute_reservation" "gce_reservation" { + name = "%s" + zone = "us-central1-f" + + specific_reservation { + count = 1 + instance_properties { + machine_type = "n1-standard-1" + } + } + + specific_reservation_required = false + depends_on = [google_project_service.compute] +} + +resource "google_container_cluster" "with_node_config" { + name = "%s" + location = "us-central1-f" + initial_node_count = 1 + + node_config { + machine_type = "n1-standard-1" + disk_size_gb = 15 + disk_type = "pd-ssd" + oauth_scopes = [ + "https://www.googleapis.com/auth/monitoring", + "https://www.googleapis.com/auth/compute", + "https://www.googleapis.com/auth/devstorage.read_only", + "https://www.googleapis.com/auth/logging.write", + ] + service_account = "default" + metadata = { + foo = "bar" + disable-legacy-endpoints = "true" + } + labels = { + foo = "bar" + } + tags = ["foo", "bar"] + + // Updatable fields + image_type = "COS_CONTAINERD" + + reservation_affinity { + consume_reservation_type = "ANY_RESERVATION_THEN_FAIL" + } + } + network = "%s" + subnetwork = "%s" + + deletion_protection = false + depends_on = [google_project_service.container] +} +`, reservation, clusterName, networkName, subnetworkName) +} + func testAccContainerCluster_withNodeConfigNodeImageConfig(clusterName, networkName, subnetworkName, imageName, imageProject string) string { return fmt.Sprintf(` diff --git a/mmv1/third_party/terraform/services/container/resource_container_node_pool_test.go.tmpl b/mmv1/third_party/terraform/services/container/resource_container_node_pool_test.go.tmpl index 5c942156fba0..b19067fe41af 100644 --- a/mmv1/third_party/terraform/services/container/resource_container_node_pool_test.go.tmpl +++ b/mmv1/third_party/terraform/services/container/resource_container_node_pool_test.go.tmpl @@ -780,6 +780,38 @@ func TestAccContainerNodePool_withReservationAffinitySpecific(t *testing.T) { }) } +func TestAccContainerNodePool_withReservationAffinityAnyReservationThenFail(t *testing.T) { + t.Parallel() + + cluster := fmt.Sprintf("tf-test-cluster-%s", acctest.RandString(t, 10)) + reservation := fmt.Sprintf("tf-test-reservation-%s", acctest.RandString(t, 10)) + np := fmt.Sprintf("tf-test-np-%s", acctest.RandString(t, 10)) + networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") + subnetworkName := tpgcompute.BootstrapSubnet(t, "gke-cluster", networkName) + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccCheckContainerClusterDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccContainerNodePool_withReservationAffinityAnyReservationThenFail(cluster, reservation, np, networkName, subnetworkName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_container_node_pool.with_reservation_affinity", + "node_config.0.reservation_affinity.#", "1"), + resource.TestCheckResourceAttr("google_container_node_pool.with_reservation_affinity", + "node_config.0.reservation_affinity.0.consume_reservation_type", "ANY_RESERVATION_THEN_FAIL"), + ), + }, + { + ResourceName: "google_container_node_pool.with_reservation_affinity", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + func TestAccContainerNodePool_withWorkloadIdentityConfig(t *testing.T) { t.Parallel() @@ -3972,6 +4004,55 @@ resource "google_container_node_pool" "with_reservation_affinity" { `, cluster, networkName, subnetworkName, reservation, np) } +func testAccContainerNodePool_withReservationAffinityAnyReservationThenFail(cluster, reservation, np, networkName, subnetworkName string) string { + return fmt.Sprintf(` +data "google_container_engine_versions" "central1a" { + location = "us-central1-a" +} + +resource "google_container_cluster" "cluster" { + name = "%s" + location = "us-central1-a" + initial_node_count = 1 + min_master_version = data.google_container_engine_versions.central1a.latest_master_version + deletion_protection = false + network = "%s" + subnetwork = "%s" +} + +resource "google_compute_reservation" "gce_reservation" { + name = "%s" + zone = "us-central1-a" + + specific_reservation { + count = 1 + instance_properties { + machine_type = "n1-standard-1" + } + } + + specific_reservation_required = false +} + +resource "google_container_node_pool" "with_reservation_affinity" { + name = "%s" + location = "us-central1-a" + cluster = google_container_cluster.cluster.name + initial_node_count = 1 + node_config { + machine_type = "n1-standard-1" + oauth_scopes = [ + "https://www.googleapis.com/auth/logging.write", + "https://www.googleapis.com/auth/monitoring", + ] + reservation_affinity { + consume_reservation_type = "ANY_RESERVATION_THEN_FAIL" + } + } +} +`, cluster, networkName, subnetworkName, reservation, np) +} + func testAccContainerNodePool_withWorkloadMetadataConfig(cluster, np, networkName, subnetworkName string) string { return fmt.Sprintf(` diff --git a/mmv1/third_party/terraform/website/docs/r/container_cluster.html.markdown b/mmv1/third_party/terraform/website/docs/r/container_cluster.html.markdown index 49ae98d1306e..349048403f24 100644 --- a/mmv1/third_party/terraform/website/docs/r/container_cluster.html.markdown +++ b/mmv1/third_party/terraform/website/docs/r/container_cluster.html.markdown @@ -1507,8 +1507,9 @@ not. * `"UNSPECIFIED"`: Default value. This should not be used. * `"NO_RESERVATION"`: Do not consume from any reserved capacity. - * `"ANY_RESERVATION"`: Consume any reservation available. + * `"ANY_RESERVATION"`: Consume any non-specific reservation available, with a fallback to on-demand capacity in case of none reservaition being claimable. * `"SPECIFIC_RESERVATION"`: Must consume from a specific reservation. Must specify key value fields for specifying the reservations. + * `"ANY_RESERVATION_THEN_FAIL"`: Consume any non-specific reservation available, without a fallback to on-demand capacity in case of none reservaition being claimable. * `key` (Optional) The label key of a reservation resource. To target a SPECIFIC_RESERVATION by name, specify "compute.googleapis.com/reservation-name" as the key and specify the name of your reservation as its value. * `values` (Optional) The list of label values of reservation resources. For example: the name of the specific reservation when using a key of "compute.googleapis.com/reservation-name" diff --git a/mmv1/third_party/terraform/website/docs/r/container_node_pool.html.markdown b/mmv1/third_party/terraform/website/docs/r/container_node_pool.html.markdown index 66af863477ac..363ec15c6c49 100644 --- a/mmv1/third_party/terraform/website/docs/r/container_node_pool.html.markdown +++ b/mmv1/third_party/terraform/website/docs/r/container_node_pool.html.markdown @@ -322,8 +322,9 @@ cluster. * `"UNSPECIFIED"`: Default value. This should not be used. * `"NO_RESERVATION"`: Do not consume from any reserved capacity. - * `"ANY_RESERVATION"`: Consume any reservation available. + * `"ANY_RESERVATION"`: Consume any non-specific reservation available, with a fallback to on-demand capacity in case of none reservaition being claimable. * `"SPECIFIC_RESERVATION"`: Must consume from a specific reservation. Must specify key value fields for specifying the reservations. + * `"ANY_RESERVATION_THEN_FAIL"`: Consume any non-specific reservation available, without a fallback to on-demand capacity in case of none reservaition being claimable. * `key` (Optional) The label key of a reservation resource. To target a SPECIFIC_RESERVATION by name, specify "compute.googleapis.com/reservation-name" as the key and specify the name of your reservation as its value. * `values` (Optional) The list of label values of reservation resources. For example: the name of the specific reservation when using a key of "compute.googleapis.com/reservation-name" From 3afa421deff5c23018a0fe61f694650220886306 Mon Sep 17 00:00:00 2001 From: "Stephen Lewis (Burrows)" Date: Fri, 26 Jun 2026 14:57:50 -0700 Subject: [PATCH 04/78] Added review pause for melinath (#18113) --- .ci/magician/github/membership_data.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.ci/magician/github/membership_data.go b/.ci/magician/github/membership_data.go index ad298184e6c4..91561d1a1fb7 100644 --- a/.ci/magician/github/membership_data.go +++ b/.ci/magician/github/membership_data.go @@ -91,12 +91,8 @@ var ( "melinath": { vacations: []Vacation{ { - startDate: newDate(2026, 3, 31), - endDate: newDate(2026, 4, 5), - }, - { - startDate: newDate(2026, 4, 10), - endDate: newDate(2026, 4, 12), + startDate: newDate(2026, 6, 26), + endDate: newDate(2026, 7, 6), }, }, }, From 8491e250a53fae5c8572e604bdb63e33124116a8 Mon Sep 17 00:00:00 2001 From: Shuya Ma <87669292+shuyama1@users.noreply.github.com> Date: Fri, 26 Jun 2026 15:09:46 -0700 Subject: [PATCH 05/78] Updated migration tool to support eap (#18109) --- tools/resource-template-converter/README.md | 2 + .../cmd/convert_resource_template.go | 35 ++-- .../migrate/format.go | 1 + .../migrate/migrate.go | 41 ++++- .../migrate/migrate_test.go | 164 +++++++++++++++++- 5 files changed, 220 insertions(+), 23 deletions(-) diff --git a/tools/resource-template-converter/README.md b/tools/resource-template-converter/README.md index 17716913bc70..1cfcf35b7655 100644 --- a/tools/resource-template-converter/README.md +++ b/tools/resource-template-converter/README.md @@ -40,6 +40,8 @@ go build -o bin/convert-resource-template main.go * `-P, --skip-product ` (Optional): Comma-separated list of product directories to skip from migration. * `--only-migration` (Optional): Run only the migration steps (examples -> samples conversion, copy and migrate templates). Do not sort keys or format string quotes. Cannot be combined with `--only-format`. * `--only-format` (Optional): Run only the formatting steps (sort keys, strip string quotes). Do not migrate examples to samples or copy templates. Cannot be combined with `--only-migration`. +* `--explicit-config-path` (Optional): Force writing `config_path` in step definition mappings during migration, even if they match the default generated template path or were not explicitly defined in the original YAML file. +* `--eap` (Optional): Enable EAP private overrides repository migration. Forces EAP folder layout resolution and forces `--explicit-config-path` to be true. * `--skip-open-pr` (Optional): Skip files modified by active open PRs updated in the last N days (configured by `--skip-open-pr-days`). * `--skip-open-pr-days ` (Optional): Number of days of open PR history to verify when checking open PRs (defaults to `60`). diff --git a/tools/resource-template-converter/cmd/convert_resource_template.go b/tools/resource-template-converter/cmd/convert_resource_template.go index 65b10182faf5..8ed3b8703a71 100644 --- a/tools/resource-template-converter/cmd/convert_resource_template.go +++ b/tools/resource-template-converter/cmd/convert_resource_template.go @@ -24,6 +24,8 @@ var onlyMigration bool var onlyFormat bool var skipOpenPR bool var skipOpenPRDays int +var explicitConfigPath bool +var eapFlag bool var convertResourceTemplateCmd = &cobra.Command{ Use: "convert-resource-template", @@ -83,19 +85,30 @@ func exeCconvertResourceTemplate(basePath string, targetFile string) error { var productsPath, examplesSourceDir, samplesDestDir string - if _, err := os.Stat(filepath.Join(basePath, "mmv1")); err == nil { + isEAP := eapFlag + if !isEAP { + if _, err := os.Stat(filepath.Join(basePath, "products")); err == nil { + if _, err := os.Stat(filepath.Join(basePath, "mmv1")); err != nil { + isEAP = true + } + } + } + + if isEAP { + // EAP private overrides repository + productsPath = filepath.Join(basePath, "products") + examplesSourceDir = basePath + samplesDestDir = filepath.Join(basePath, "samples") + explicitConfigPath = true + } else { // Public magic-modules repository + if _, err := os.Stat(filepath.Join(basePath, "mmv1")); err != nil { + log.Fatalf("Neither 'mmv1' nor 'products' directory structure found. Please ensure this tool is run from a magic-modules or magic-modules-private-overrides directory.") + } productsPath = filepath.Join(basePath, "mmv1", "products") templatesPath := filepath.Join(basePath, "mmv1", "templates", "terraform") examplesSourceDir = filepath.Join(templatesPath, "examples") samplesDestDir = filepath.Join(templatesPath, "samples", "services") - } else if _, err := os.Stat(filepath.Join(basePath, "products")); err == nil { - // EAP private overrides repository - productsPath = filepath.Join(basePath, "products") - examplesSourceDir = basePath - samplesDestDir = filepath.Join(basePath, "templates", "terraform", "samples", "services") - } else { - log.Fatalf("Neither 'mmv1' nor 'products' directory structure found. Please ensure this tool is run from a magic-modules or magic-modules-private-overrides directory.") } var pathsToWalk []string @@ -194,7 +207,7 @@ func exeCconvertResourceTemplate(basePath string, targetFile string) error { return fmt.Errorf("error copying templates for %s: %w", t.resolvedPath, err) } } - if err := migrate.MigrateFile(t.resolvedPath, t.serviceName, onlyMigration, onlyFormat); err != nil { + if err := migrate.MigrateFile(t.resolvedPath, t.serviceName, onlyMigration, onlyFormat, explicitConfigPath, isEAP); err != nil { return fmt.Errorf("failed to migrate file %s: %w", t.resolvedPath, err) } } @@ -246,7 +259,7 @@ func exeCconvertResourceTemplate(basePath string, targetFile string) error { // Continue processing other files even if one fails. } } - if err := migrate.MigrateFile(path, serviceName, onlyMigration, onlyFormat); err != nil { + if err := migrate.MigrateFile(path, serviceName, onlyMigration, onlyFormat, explicitConfigPath, isEAP); err != nil { log.Printf("Failed to migrate file %s: %v\n", path, err) // Continue migrating other files even if one fails. } @@ -270,6 +283,8 @@ func init() { convertResourceTemplateCmd.Flags().StringVarP(&skipProductsFlag, "skip-product", "P", "", "Comma-separated list of product directories to skip from migration") convertResourceTemplateCmd.Flags().BoolVar(&onlyMigration, "only-migration", false, "Only run migration steps (examples -> samples, copy templates), skip formatting") convertResourceTemplateCmd.Flags().BoolVar(&onlyFormat, "only-format", false, "Only run formatting steps (sort keys, strip quotes), skip migration") + convertResourceTemplateCmd.Flags().BoolVar(&explicitConfigPath, "explicit-config-path", false, "Explicitly write config_path for all samples during migration, even if they match default paths") + convertResourceTemplateCmd.Flags().BoolVar(&eapFlag, "eap", false, "Enable EAP private overrides repository migration (forces EAP directory structure and explicit config_path)") convertResourceTemplateCmd.Flags().BoolVar(&skipOpenPR, "skip-open-pr", false, "Skip files modified by active open PRs updated in the last N days (configured by --skip-open-pr-days)") convertResourceTemplateCmd.Flags().IntVar(&skipOpenPRDays, "skip-open-pr-days", 60, "Number of days of open PR history to verify when checking open PRs") rootCmd.AddCommand(convertResourceTemplateCmd) diff --git a/tools/resource-template-converter/migrate/format.go b/tools/resource-template-converter/migrate/format.go index b3bd5ab91cce..4f7d64115c7d 100644 --- a/tools/resource-template-converter/migrate/format.go +++ b/tools/resource-template-converter/migrate/format.go @@ -58,6 +58,7 @@ func init() { nestedKeyOrders["operation"] = getYamlStructFieldOrder(api.Operation{}) nestedKeyOrders["iam_policy"] = getYamlStructFieldOrder(resource.IamPolicy{}) nestedKeyOrders["custom_code"] = getYamlStructFieldOrder(resource.CustomCode{}) + nestedKeyOrders["steps"] = getYamlStructFieldOrder(resource.Step{}) } func sortMappingNode(mappingNode *yaml.Node, keyOrder []string) { diff --git a/tools/resource-template-converter/migrate/migrate.go b/tools/resource-template-converter/migrate/migrate.go index 5fa7cd13b6c1..3ee8a71b9c86 100644 --- a/tools/resource-template-converter/migrate/migrate.go +++ b/tools/resource-template-converter/migrate/migrate.go @@ -15,7 +15,7 @@ import ( // PATCH-START: existing structs // PATCH-END: existing structs -func MigrateFile(filePath, serviceName string, onlyMigration, onlyFormat bool) error { +func MigrateFile(filePath, serviceName string, onlyMigration, onlyFormat, explicitConfigPath, isEAP bool) error { originalBytes, err := ioutil.ReadFile(filePath) if err != nil { return fmt.Errorf("failed to read file: %w", err) @@ -83,17 +83,29 @@ func MigrateFile(filePath, serviceName string, onlyMigration, onlyFormat bool) e } var newConfigPath string - if configPathVal != "" && serviceName != "" { - templateName := filepath.Base(configPathVal) - calculatedPath := path.Join("templates/terraform/samples/services", serviceName, templateName) - defaultPath := path.Join("templates/terraform/samples/services", serviceName, fmt.Sprintf("%s.tf.tmpl", nameVal)) - if calculatedPath != defaultPath { + if serviceName != "" { + var templateName string + if configPathVal != "" { + templateName = filepath.Base(configPathVal) + } else { + templateName = fmt.Sprintf("%s.tf.tmpl", nameVal) + } + var calculatedPath, defaultPath string + if isEAP { + calculatedPath = path.Join("samples", serviceName, templateName) + defaultPath = path.Join("samples", serviceName, fmt.Sprintf("%s.tf.tmpl", nameVal)) + } else { + calculatedPath = path.Join("templates/terraform/samples/services", serviceName, templateName) + defaultPath = path.Join("templates/terraform/samples/services", serviceName, fmt.Sprintf("%s.tf.tmpl", nameVal)) + } + if explicitConfigPath || calculatedPath != defaultPath { newConfigPath = calculatedPath } } samplesContent := []*yaml.Node{} stepContent := []*yaml.Node{} + hasConfigPath := false for i := 0; i < len(exampleMapNode.Content); i += 2 { keyNode := exampleMapNode.Content[i] @@ -109,6 +121,7 @@ func MigrateFile(filePath, serviceName string, onlyMigration, onlyFormat bool) e stepContent = append(stepContent, cloneNode(keyNode), cloneNode(valNode)) case "config_path": + hasConfigPath = true if newConfigPath != "" { valNode.Value = newConfigPath valNode.Style = 0 @@ -136,6 +149,20 @@ func MigrateFile(filePath, serviceName string, onlyMigration, onlyFormat bool) e } } + if explicitConfigPath && !hasConfigPath && newConfigPath != "" { + keyNode := &yaml.Node{ + Kind: yaml.ScalarNode, + Tag: "!!str", + Value: "config_path", + } + valNode := &yaml.Node{ + Kind: yaml.ScalarNode, + Tag: "!!str", + Value: newConfigPath, + } + stepContent = append(stepContent, keyNode, valNode) + } + // Construct Step Mapping Node stepMapNode := &yaml.Node{ Kind: yaml.MappingNode, @@ -143,6 +170,8 @@ func MigrateFile(filePath, serviceName string, onlyMigration, onlyFormat bool) e Content: stepContent, } + sortMappingNode(stepMapNode, nestedKeyOrders["steps"]) + // Construct Steps Sequence Node stepsSeqNode := &yaml.Node{ Kind: yaml.SequenceNode, diff --git a/tools/resource-template-converter/migrate/migrate_test.go b/tools/resource-template-converter/migrate/migrate_test.go index 4da1e24e796d..92c1c3d19b79 100644 --- a/tools/resource-template-converter/migrate/migrate_test.go +++ b/tools/resource-template-converter/migrate/migrate_test.go @@ -53,7 +53,7 @@ examples: } // Run migration - err = MigrateFile(yamlPath, "accesscontextmanager", false, false) + err = MigrateFile(yamlPath, "accesscontextmanager", false, false, false, false) if err != nil { t.Fatalf("MigrateFile failed: %v", err) } @@ -122,7 +122,7 @@ examples: } // Run migration - err = MigrateFile(yamlPath, "accesscontextmanager", false, false) + err = MigrateFile(yamlPath, "accesscontextmanager", false, false, false, false) if err != nil { t.Fatalf("MigrateFile failed: %v", err) } @@ -187,7 +187,7 @@ examples: } // Run migration - err = MigrateFile(yamlPath, "accesscontextmanager", false, false) + err = MigrateFile(yamlPath, "accesscontextmanager", false, false, false, false) if err != nil { t.Fatalf("MigrateFile failed: %v", err) } @@ -249,7 +249,7 @@ examples: } // Run migration - err = MigrateFile(yamlPath, "datacatalog", false, false) + err = MigrateFile(yamlPath, "datacatalog", false, false, false, false) if err != nil { t.Fatalf("MigrateFile failed: %v", err) } @@ -305,7 +305,7 @@ examples: } // Run migration - err = MigrateFile(yamlPath, "artifactregistry", false, false) + err = MigrateFile(yamlPath, "artifactregistry", false, false, false, false) if err != nil { t.Fatalf("MigrateFile failed: %v", err) } @@ -354,7 +354,7 @@ examples: ioutil.WriteFile(tmplPath, []byte(`resource "google" "test" {}`), 0644) // Run migration only - err = MigrateFile(yamlPath, "accesscontextmanager", true, false) + err = MigrateFile(yamlPath, "accesscontextmanager", true, false, false, false) if err != nil { t.Fatalf("MigrateFile failed: %v", err) } @@ -394,7 +394,7 @@ description: "An AccessLevel is a label." ioutil.WriteFile(yamlPath, []byte(yamlContent), 0644) // Run formatting only - err = MigrateFile(yamlPath, "accesscontextmanager", false, true) + err = MigrateFile(yamlPath, "accesscontextmanager", false, true, false, false) if err != nil { t.Fatalf("MigrateFile failed: %v", err) } @@ -414,3 +414,153 @@ description: "An AccessLevel is a label." t.Errorf("expected string quotes to be stripped under only-format, got: %s", updatedYaml) } } + +func TestMigrateFile_ExplicitConfigPath(t *testing.T) { + // Tests that when explicitConfigPath is enabled, config_path is explicitly written + // even if it matches the default templates path, or even if the original yaml didn't have it. + tmpDir, err := ioutil.TempDir("", "mm-explicit-config-test") + if err != nil { + t.Fatalf("failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + productsDir := filepath.Join(tmpDir, "mmv1", "products", "accesscontextmanager") + os.MkdirAll(productsDir, 0755) + + examplesDir := filepath.Join(tmpDir, "mmv1", "templates", "terraform", "examples") + os.MkdirAll(examplesDir, 0755) + + // Create resource YAML file (without config_path explicitly set) + yamlPath := filepath.Join(productsDir, "AccessLevel.yaml") + yamlContent := `--- +name: AccessLevel +examples: + - name: access_context_manager_access_level_basic + primary_resource_id: access-level + vars: + access_level_name: chromeos_no_lock +` + ioutil.WriteFile(yamlPath, []byte(yamlContent), 0644) + + tmplPath := filepath.Join(examplesDir, "access_context_manager_access_level_basic.tf.tmpl") + ioutil.WriteFile(tmplPath, []byte(`resource "google" "test" {}`), 0644) + + // Run migration with explicitConfigPath = true + err = MigrateFile(yamlPath, "accesscontextmanager", false, false, true, false) + if err != nil { + t.Fatalf("MigrateFile failed: %v", err) + } + + updatedYamlBytes, _ := ioutil.ReadFile(yamlPath) + updatedYaml := string(updatedYamlBytes) + + // Verify YAML content has config_path explicitly set to default samples path, ordered right after name: + expectedSubstr := ` steps: + - name: access_context_manager_access_level_basic + config_path: templates/terraform/samples/services/accesscontextmanager/access_context_manager_access_level_basic.tf.tmpl + resource_id_vars:` + if !strings.Contains(updatedYaml, expectedSubstr) { + t.Errorf("expected sorted keys and config_path right after name. Expected substring:\n%s\n\nGot:\n%s", expectedSubstr, updatedYaml) + } +} + +func TestMigrateFile_OnlyMigrationWithExplicitConfigPath(t *testing.T) { + tmpDir, err := ioutil.TempDir("", "mm-only-migration-explicit-config-test") + if err != nil { + t.Fatalf("failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + productsDir := filepath.Join(tmpDir, "mmv1", "products", "accesscontextmanager") + os.MkdirAll(productsDir, 0755) + + examplesDir := filepath.Join(tmpDir, "mmv1", "templates", "terraform", "examples") + os.MkdirAll(examplesDir, 0755) + + // Create resource YAML file + yamlPath := filepath.Join(productsDir, "AccessLevel.yaml") + yamlContent := `--- +name: "AccessLevel" +examples: + - name: access_context_manager_access_level_basic + primary_resource_id: access-level + vars: + access_level_name: chromeos_no_lock +` + ioutil.WriteFile(yamlPath, []byte(yamlContent), 0644) + + tmplPath := filepath.Join(examplesDir, "access_context_manager_access_level_basic.tf.tmpl") + ioutil.WriteFile(tmplPath, []byte(`resource "google" "test" {}`), 0644) + + // Run migration only + err = MigrateFile(yamlPath, "accesscontextmanager", true, false, true, false) + if err != nil { + t.Fatalf("MigrateFile failed: %v", err) + } + + updatedYamlBytes, _ := ioutil.ReadFile(yamlPath) + updatedYaml := string(updatedYamlBytes) + + // Quotes should still be preserved: + if !strings.Contains(updatedYaml, `name: "AccessLevel"`) { + t.Errorf("expected name quotes to be preserved, got: %s", updatedYaml) + } + + // But step key sorting should still happen (config_path sorted right after name): + expectedSubstr := ` steps: + - name: access_context_manager_access_level_basic + config_path: templates/terraform/samples/services/accesscontextmanager/access_context_manager_access_level_basic.tf.tmpl + resource_id_vars:` + if !strings.Contains(updatedYaml, expectedSubstr) { + t.Errorf("expected sorted keys inside steps under only-migration. Expected substring:\n%s\n\nGot:\n%s", expectedSubstr, updatedYaml) + } +} + +func TestMigrateFile_EAP(t *testing.T) { + // Tests that when isEAP is true, config_path is resolved flatly as samples/ + // and explicitly written. + tmpDir, err := ioutil.TempDir("", "mm-eap-test") + if err != nil { + t.Fatalf("failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + productsDir := filepath.Join(tmpDir, "products", "accesscontextmanager") + os.MkdirAll(productsDir, 0755) + + examplesDir := filepath.Join(tmpDir, "examples") + os.MkdirAll(examplesDir, 0755) + + // Create resource YAML file (without config_path explicitly set) + yamlPath := filepath.Join(productsDir, "AccessLevel.yaml") + yamlContent := `--- +name: AccessLevel +examples: + - name: access_context_manager_access_level_basic + primary_resource_id: access-level + vars: + access_level_name: chromeos_no_lock +` + ioutil.WriteFile(yamlPath, []byte(yamlContent), 0644) + + tmplPath := filepath.Join(examplesDir, "access_context_manager_access_level_basic.tf.tmpl") + ioutil.WriteFile(tmplPath, []byte(`resource "google" "test" {}`), 0644) + + // Run migration with isEAP = true, explicitConfigPath = true + err = MigrateFile(yamlPath, "accesscontextmanager", false, false, true, true) + if err != nil { + t.Fatalf("MigrateFile failed: %v", err) + } + + updatedYamlBytes, _ := ioutil.ReadFile(yamlPath) + updatedYaml := string(updatedYamlBytes) + + // Verify YAML content has nested config_path under samples/serviceName/: + expectedSubstr := ` steps: + - name: access_context_manager_access_level_basic + config_path: samples/accesscontextmanager/access_context_manager_access_level_basic.tf.tmpl + resource_id_vars:` + if !strings.Contains(updatedYaml, expectedSubstr) { + t.Errorf("expected nested EAP config_path under samples/. Expected substring:\n%s\n\nGot:\n%s", expectedSubstr, updatedYaml) + } +} From 54f5e8ba6602cdda4f81752d2568eb8e868612fe Mon Sep 17 00:00:00 2001 From: Majed Alfaraj <98921162+majedalfaraj@users.noreply.github.com> Date: Fri, 26 Jun 2026 15:43:47 -0700 Subject: [PATCH 06/78] test(magician): add interceptingRunner to reroute sandbox executions (#17961) --- .ci/magician/cmd/sandbox_sanity_test.go | 38 +++++++++++ .ci/magician/cmd/sandbox_test.go | 86 ++++++++++++++++++++++--- 2 files changed, 115 insertions(+), 9 deletions(-) diff --git a/.ci/magician/cmd/sandbox_sanity_test.go b/.ci/magician/cmd/sandbox_sanity_test.go index c6327a665567..84a9e3dfe2ae 100644 --- a/.ci/magician/cmd/sandbox_sanity_test.go +++ b/.ci/magician/cmd/sandbox_sanity_test.go @@ -17,6 +17,7 @@ package cmd import ( "os" + "path/filepath" "strings" "testing" ) @@ -28,6 +29,7 @@ func TestSandboxInitialization(t *testing.T) { t.Fatalf("Sandbox directory %s does not exist", sb.Dir) } + sb.AllowPassthrough("git") output, err := sb.Runner.Run("git", []string{"status"}, nil) if err != nil { t.Fatalf("Sandbox is not a valid git repository: %v\nOutput: %s", err, output) @@ -37,3 +39,39 @@ func TestSandboxInitialization(t *testing.T) { t.Errorf("Expected 'On branch main' in git status output, got: %s", output) } } + +func TestSandboxInterceptor_AllowedCommand(t *testing.T) { + sb := newSandbox(t) + sb.RequireWhitelist() + _, err := sb.Runner.Run("touch", []string{"test_file.txt"}, nil) + if err != nil { + t.Fatalf("Expected whitelisted command to succeed, got: %v", err) + } +} + +func TestSandboxInterceptor_MockOverridesAllowed(t *testing.T) { + sb := newSandbox(t) + sb.RequireWhitelist() + sb.Runner.WriteFile("echo", "#!/bin/bash\necho 'intercepted echo'") + os.Chmod(filepath.Join(sb.Dir, "echo"), 0755) + + output, err := sb.Runner.Run("echo", []string{"hello"}, nil) + if err != nil { + t.Fatalf("Expected mocked command to succeed, got: %v", err) + } + if !strings.Contains(output, "intercepted echo") { + t.Errorf("Expected sandbox mock to override whitelist, got: %s", output) + } +} + +func TestSandboxInterceptor_UnhandledCommandPanics(t *testing.T) { + sb := newSandbox(t) + sb.RequireWhitelist() + _, err := sb.Runner.Run("curl", []string{"http://example.com"}, nil) + if err == nil { + t.Fatalf("Expected unhandled command to return error, but it succeeded") + } + if !strings.Contains(err.Error(), "not in the passthrough list") { + t.Errorf("Expected strict interceptor error, got: %v", err) + } +} diff --git a/.ci/magician/cmd/sandbox_test.go b/.ci/magician/cmd/sandbox_test.go index 444f100bef98..04c0dfd0454c 100644 --- a/.ci/magician/cmd/sandbox_test.go +++ b/.ci/magician/cmd/sandbox_test.go @@ -16,6 +16,10 @@ package cmd import ( + "fmt" + "os" + "path/filepath" + "strings" "testing" "magician/exec" @@ -26,29 +30,93 @@ type sandbox struct { Runner ExecRunner } +// Routes some commands to fake sandbox scripts, bypassing $PATH. +type interceptingRunner struct { + *exec.Runner + sbDir string + allowedPassthrough map[string]bool + strictMode bool +} + +func (s *interceptingRunner) Run(name string, args []string, env map[string]string) (string, error) { + if !s.strictMode { + if !strings.Contains(name, string(filepath.Separator)) { + sandboxBin := filepath.Join(s.sbDir, name) + if _, err := os.Stat(sandboxBin); err == nil { + name = sandboxBin + } + } + return s.Runner.Run(name, args, env) + } + + baseName := filepath.Base(name) + + sandboxBin := filepath.Join(s.sbDir, baseName) + if _, err := os.Stat(sandboxBin); err == nil { + return s.Runner.Run(sandboxBin, args, env) + } + + if s.allowedPassthrough[baseName] { + return s.Runner.Run(name, args, env) + } + + return "", fmt.Errorf("command %q is not in the passthrough list and no sandbox mock was found. Please mock it or add it to the passthrough list", baseName) +} + func newSandbox(t *testing.T) *sandbox { dir := t.TempDir() - runner, err := exec.NewRunner() + realRunner, err := exec.NewRunner() if err != nil { t.Fatalf("Failed to create runner: %v", err) } - if err := runner.PushDir(dir); err != nil { + if err := realRunner.PushDir(dir); err != nil { t.Fatalf("Failed to push dir: %v", err) } - runner.MustRun("git", []string{"init", "-b", "main"}, nil) + realRunner.MustRun("git", []string{"init", "-b", "main"}, nil) + + realRunner.MustRun("touch", []string{"README.md"}, nil) + realRunner.MustRun("git", []string{"add", "."}, nil) + realRunner.MustRun("git", []string{"config", "user.email", "test@example.com"}, nil) + realRunner.MustRun("git", []string{"config", "user.name", "Test Sandbox"}, nil) + realRunner.MustRun("git", []string{"config", "commit.gpgsign", "false"}, nil) + realRunner.MustRun("git", []string{"commit", "-m", "Initial sandbox commit"}, nil) - runner.MustRun("touch", []string{"README.md"}, nil) - runner.MustRun("git", []string{"add", "."}, nil) - runner.MustRun("git", []string{"config", "user.email", "test@example.com"}, nil) - runner.MustRun("git", []string{"config", "user.name", "Test Sandbox"}, nil) - runner.MustRun("git", []string{"config", "commit.gpgsign", "false"}, nil) - runner.MustRun("git", []string{"commit", "-m", "Initial sandbox commit"}, nil) + runner := &interceptingRunner{ + Runner: realRunner, + sbDir: dir, + allowedPassthrough: map[string]bool{ + "chmod": true, + "rm": true, + "mkdir": true, + "touch": true, + "cp": true, + "mv": true, + "ls": true, + "echo": true, + "cat": true, + "grep": true, + }, + } return &sandbox{ Dir: dir, Runner: runner, } } + +func (s *sandbox) AllowPassthrough(commands ...string) { + if runner, ok := s.Runner.(*interceptingRunner); ok { + for _, cmd := range commands { + runner.allowedPassthrough[cmd] = true + } + } +} + +func (s *sandbox) RequireWhitelist() { + if runner, ok := s.Runner.(*interceptingRunner); ok { + runner.strictMode = true + } +} From ad6c021e51ce1feae0b4b3d0aad8f28c58daa407 Mon Sep 17 00:00:00 2001 From: Vaibhav Rau Date: Fri, 26 Jun 2026 18:13:02 -0500 Subject: [PATCH 07/78] compute: add target_type and target_forwarding_rules to google_compute_region_network_firewall_policy_with_rules (#18047) Co-authored-by: Mauricio Alvarez Leon <65101411+BBBmau@users.noreply.github.com> --- .../RegionNetworkFirewallPolicyWithRules.yaml | 23 ++++ ...rk_firewall_policy_with_rules_full.tf.tmpl | 85 +++++++++++++ ...network_firewall_policy_with_rules_test.go | 118 ++++++++++++++++++ 3 files changed, 226 insertions(+) diff --git a/mmv1/products/compute/RegionNetworkFirewallPolicyWithRules.yaml b/mmv1/products/compute/RegionNetworkFirewallPolicyWithRules.yaml index 7c11f810405c..bc1c7beb97ad 100644 --- a/mmv1/products/compute/RegionNetworkFirewallPolicyWithRules.yaml +++ b/mmv1/products/compute/RegionNetworkFirewallPolicyWithRules.yaml @@ -372,6 +372,29 @@ properties: the firewall policy rule is not enforced and traffic behaves as if it did not exist. If this is unspecified, the firewall policy rule will be enabled. + - name: 'targetType' + type: Enum + description: | + Target types of the firewall policy rule. + Default value is INSTANCES. + When target_type is INTERNAL_MANAGED_LB, target_forwarding_rules must be set + default_from_api: true + enum_values: + - 'INSTANCES' + - 'INTERNAL_MANAGED_LB' + - name: 'targetForwardingRules' + type: Array + description: | + A list of forwarding rules to which this rule applies. + This field allows you to control which load balancers get this rule. + For example, the following are valid values: + - https://www.googleapis.com/compute/v1/projects/project/global/forwardingRules/forwardingRule + - https://www.googleapis.com/compute/v1/projects/project/regions/region/forwardingRules/forwardingRule + - projects/project/global/forwardingRules/forwardingRule + - projects/project/regions/region/forwardingRules/forwardingRule + item_type: + type: String + diff_suppress_func: 'tpgresource.CompareSelfLinkRelativePaths' - name: 'predefinedRules' type: Array description: A list of firewall policy pre-defined rules. diff --git a/mmv1/templates/terraform/samples/services/compute/compute_region_network_firewall_policy_with_rules_full.tf.tmpl b/mmv1/templates/terraform/samples/services/compute/compute_region_network_firewall_policy_with_rules_full.tf.tmpl index deb1c4d4d3c7..1156196ad002 100644 --- a/mmv1/templates/terraform/samples/services/compute/compute_region_network_firewall_policy_with_rules_full.tf.tmpl +++ b/mmv1/templates/terraform/samples/services/compute/compute_region_network_firewall_policy_with_rules_full.tf.tmpl @@ -1,6 +1,71 @@ data "google_project" "project" { } +resource "google_compute_network" "target_forwarding_rule" { + name = "tf-test-network-%{random_suffix}" + auto_create_subnetworks = false +} + +resource "google_compute_subnetwork" "target_forwarding_rule_proxy_subnetwork" { + name = "tf-test-proxy-subnetwork-%{random_suffix}" + region = "us-west2" + network = google_compute_network.target_forwarding_rule.id + ip_cidr_range = "10.20.0.0/24" + purpose = "REGIONAL_MANAGED_PROXY" + role = "ACTIVE" +} + +resource "google_compute_subnetwork" "target_forwarding_rule_default_subnetwork" { + name = "tf-test-default-subnetwork-%{random_suffix}" + region = "us-west2" + network = google_compute_network.target_forwarding_rule.id + ip_cidr_range = "10.10.0.0/24" +} + +resource "google_compute_region_health_check" "target_forwarding_rule" { + name = "tf-test-health-check-%{random_suffix}" + region = "us-west2" + + http_health_check { + port = 80 + } +} + +resource "google_compute_region_backend_service" "target_forwarding_rule" { + name = "tf-test-backend-service-%{random_suffix}" + region = "us-west2" + protocol = "HTTP" + load_balancing_scheme = "INTERNAL_MANAGED" + health_checks = [google_compute_region_health_check.target_forwarding_rule.id] +} + +resource "google_compute_region_url_map" "target_forwarding_rule" { + name = "tf-test-url-map-%{random_suffix}" + region = "us-west2" + default_service = google_compute_region_backend_service.target_forwarding_rule.id +} + +resource "google_compute_region_target_http_proxy" "target_forwarding_rule" { + name = "tf-test-target-http-proxy-%{random_suffix}" + region = "us-west2" + url_map = google_compute_region_url_map.target_forwarding_rule.id +} + +resource "google_compute_forwarding_rule" "target_forwarding_rule" { + name = "tf-test-forwarding-rule-%{random_suffix}" + region = "us-west2" + network = google_compute_network.target_forwarding_rule.id + subnetwork = google_compute_subnetwork.target_forwarding_rule_default_subnetwork.id + load_balancing_scheme = "INTERNAL_MANAGED" + target = google_compute_region_target_http_proxy.target_forwarding_rule.id + ip_protocol = "TCP" + port_range = "80" + + depends_on = [ + google_compute_subnetwork.target_forwarding_rule_proxy_subnetwork, + ] +} + resource "google_compute_region_network_firewall_policy_with_rules" "{{$.PrimaryResourceId}}" { name = "{{index $.ResourceIdVars "fw_policy"}}" region = "us-west2" @@ -56,6 +121,26 @@ resource "google_compute_region_network_firewall_policy_with_rules" "{{$.Primary } } } + + rule { + description = "internal managed lb rule" + priority = 3000 + action = "allow" + direction = "INGRESS" + + target_type = "INTERNAL_MANAGED_LB" + target_forwarding_rules = [ + google_compute_forwarding_rule.target_forwarding_rule.self_link + ] + + match { + src_ip_ranges = ["10.0.0.0/8"] + + layer4_config { + ip_protocol = "tcp" + } + } + } } resource "google_network_security_address_group" "address_group_1" { diff --git a/mmv1/third_party/terraform/services/compute/resource_compute_region_network_firewall_policy_with_rules_test.go b/mmv1/third_party/terraform/services/compute/resource_compute_region_network_firewall_policy_with_rules_test.go index 14431b7836f3..d92a4c514e33 100644 --- a/mmv1/third_party/terraform/services/compute/resource_compute_region_network_firewall_policy_with_rules_test.go +++ b/mmv1/third_party/terraform/services/compute/resource_compute_region_network_firewall_policy_with_rules_test.go @@ -46,6 +46,31 @@ func TestAccComputeRegionNetworkFirewallPolicyWithRules_update(t *testing.T) { }) } +func TestAccComputeRegionNetworkFirewallPolicyWithRules_targetForwardingRules(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "random_suffix": acctest.RandString(t, 10), + } + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccCheckComputeRegionNetworkFirewallPolicyWithRulesDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccComputeRegionNetworkFirewallPolicyWithRules_targetForwardingRules(context), + }, + { + ResourceName: "google_compute_region_network_firewall_policy_with_rules.region-network-firewall-policy-with-rules", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"region"}, + }, + }, + }) +} + func testAccComputeRegionNetworkFirewallPolicyWithRules_full(context map[string]interface{}) string { return acctest.Nprintf(` data "google_project" "project" { @@ -203,3 +228,96 @@ resource "google_tags_tag_value" "secure_tag_value_1" { } `, context) } + +func testAccComputeRegionNetworkFirewallPolicyWithRules_targetForwardingRules(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_compute_network" "target_forwarding_rule" { + name = "tf-test-network-%{random_suffix}" + auto_create_subnetworks = false +} + +resource "google_compute_subnetwork" "target_forwarding_rule_proxy_subnetwork" { + name = "tf-test-proxy-subnetwork-%{random_suffix}" + region = "us-west2" + network = google_compute_network.target_forwarding_rule.id + ip_cidr_range = "10.20.0.0/24" + purpose = "REGIONAL_MANAGED_PROXY" + role = "ACTIVE" +} + +resource "google_compute_subnetwork" "target_forwarding_rule_default_subnetwork" { + name = "tf-test-default-subnetwork-%{random_suffix}" + region = "us-west2" + network = google_compute_network.target_forwarding_rule.id + ip_cidr_range = "10.10.0.0/24" +} + +resource "google_compute_region_health_check" "target_forwarding_rule" { + name = "tf-test-health-check-%{random_suffix}" + region = "us-west2" + + http_health_check { + port = 80 + } +} + +resource "google_compute_region_backend_service" "target_forwarding_rule" { + name = "tf-test-backend-service-%{random_suffix}" + region = "us-west2" + protocol = "HTTP" + load_balancing_scheme = "INTERNAL_MANAGED" + health_checks = [google_compute_region_health_check.target_forwarding_rule.id] +} + +resource "google_compute_region_url_map" "target_forwarding_rule" { + name = "tf-test-url-map-%{random_suffix}" + region = "us-west2" + default_service = google_compute_region_backend_service.target_forwarding_rule.id +} + +resource "google_compute_region_target_http_proxy" "target_forwarding_rule" { + name = "tf-test-target-http-proxy-%{random_suffix}" + region = "us-west2" + url_map = google_compute_region_url_map.target_forwarding_rule.id +} + +resource "google_compute_forwarding_rule" "target_forwarding_rule" { + name = "tf-test-forwarding-rule-%{random_suffix}" + region = "us-west2" + network = google_compute_network.target_forwarding_rule.id + subnetwork = google_compute_subnetwork.target_forwarding_rule_default_subnetwork.id + load_balancing_scheme = "INTERNAL_MANAGED" + target = google_compute_region_target_http_proxy.target_forwarding_rule.id + ip_protocol = "TCP" + port_range = "80" + + depends_on = [ + google_compute_subnetwork.target_forwarding_rule_proxy_subnetwork, + ] +} + +resource "google_compute_region_network_firewall_policy_with_rules" "region-network-firewall-policy-with-rules" { + name = "tf-test-tf-region-fw-policy-with-rules-lb%{random_suffix}" + region = "us-west2" + description = "Terraform test target forwarding rules" + + rule { + priority = 1000 + action = "allow" + direction = "INGRESS" + target_type = "INTERNAL_MANAGED_LB" + target_forwarding_rules = [ + google_compute_forwarding_rule.target_forwarding_rule.self_link, + ] + + match { + src_ip_ranges = ["10.0.0.0/8"] + + layer4_config { + ip_protocol = "tcp" + } + } + } +} +`, context) +} From 67838c056aef0d78ef642f83fd900df2a66de0a7 Mon Sep 17 00:00:00 2001 From: Riley Karson Date: Mon, 29 Jun 2026 08:14:38 -0700 Subject: [PATCH 08/78] Add vacation update (#18119) --- .ci/magician/github/membership_data.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.ci/magician/github/membership_data.go b/.ci/magician/github/membership_data.go index 91561d1a1fb7..c147312c10d5 100644 --- a/.ci/magician/github/membership_data.go +++ b/.ci/magician/github/membership_data.go @@ -118,6 +118,10 @@ var ( startDate: newDate(2026, 06, 01), endDate: newDate(2026, 06, 12), }, + { + startDate: newDate(2026, 06, 29), + endDate: newDate(2026, 07, 06), + }, }, }, "roaks3": { From 4a7954a28a8762384d8759e028f008471bfd638f Mon Sep 17 00:00:00 2001 From: Nikita Samartsev Date: Mon, 29 Jun 2026 17:48:29 +0200 Subject: [PATCH 09/78] Migrate `expandAdvancedMachineFeatures`, `flattenAdvancedMachineFeatures` functions in `compute_instance_helpers.go.tmpl` to use direct HTTP rather than a client library (#17723) --- .../compute/compute_instance_helpers.go.tmpl | 49 ++++++++++++++++++- .../compute/resource_compute_instance.go.tmpl | 10 ++-- ...resource_compute_instance_template.go.tmpl | 4 +- ...e_compute_region_instance_template.go.tmpl | 8 +-- .../services/compute/compute_instance.go.tmpl | 2 +- .../compute/compute_instance_tfplan2cai.go | 2 +- 6 files changed, 56 insertions(+), 19 deletions(-) diff --git a/mmv1/third_party/terraform/services/compute/compute_instance_helpers.go.tmpl b/mmv1/third_party/terraform/services/compute/compute_instance_helpers.go.tmpl index 3e711c68a15e..72034159cf5f 100644 --- a/mmv1/third_party/terraform/services/compute/compute_instance_helpers.go.tmpl +++ b/mmv1/third_party/terraform/services/compute/compute_instance_helpers.go.tmpl @@ -1187,7 +1187,52 @@ func flattenConfidentialInstanceConfig(ConfidentialInstanceConfig map[string]int {{"}}"}} } -func expandAdvancedMachineFeatures(d tpgresource.TerraformResourceData) *compute.AdvancedMachineFeatures { +func expandAdvancedMachineFeatures(d tpgresource.TerraformResourceData) map[string]interface{} { + if _, ok := d.GetOk("advanced_machine_features"); !ok { + return nil + } + + prefix := "advanced_machine_features.0" + result := map[string]interface{}{} + if v := d.Get(prefix + ".enable_nested_virtualization").(bool); v { + result["enableNestedVirtualization"] = v + } + if v := d.Get(prefix + ".threads_per_core").(int); v != 0 { + result["threadsPerCore"] = int64(v) + } + if v := d.Get(prefix + ".turbo_mode").(string); v != "" { + result["turboMode"] = v + } + if v := d.Get(prefix + ".visible_core_count").(int); v != 0 { + result["visibleCoreCount"] = int64(v) + } + if v := d.Get(prefix + ".performance_monitoring_unit").(string); v != "" { + result["performanceMonitoringUnit"] = v + } + if v := d.Get(prefix + ".enable_uefi_networking").(bool); v { + result["enableUefiNetworking"] = v + } + return result +} + +func flattenAdvancedMachineFeatures(v map[string]interface{}) []map[string]interface{} { + if v == nil { + return nil + } + threadsPerCore, _ := v["threadsPerCore"].(float64) + visibleCoreCount, _ := v["visibleCoreCount"].(float64) + result := map[string]interface{}{ + "enable_nested_virtualization": v["enableNestedVirtualization"], + "threads_per_core": int64(threadsPerCore), + "turbo_mode": v["turboMode"], + "visible_core_count": int64(visibleCoreCount), + "performance_monitoring_unit": v["performanceMonitoringUnit"], + "enable_uefi_networking": v["enableUefiNetworking"], + } + return []map[string]interface{}{result} +} + +func expandAdvancedMachineFeaturesTyped(d tpgresource.TerraformResourceData) *compute.AdvancedMachineFeatures { if _, ok := d.GetOk("advanced_machine_features"); !ok { return nil } @@ -1203,7 +1248,7 @@ func expandAdvancedMachineFeatures(d tpgresource.TerraformResourceData) *compute } } -func flattenAdvancedMachineFeatures(AdvancedMachineFeatures *compute.AdvancedMachineFeatures) []map[string]interface{} { +func flattenAdvancedMachineFeaturesTyped(AdvancedMachineFeatures *compute.AdvancedMachineFeatures) []map[string]interface{} { if AdvancedMachineFeatures == nil { return nil } diff --git a/mmv1/third_party/terraform/services/compute/resource_compute_instance.go.tmpl b/mmv1/third_party/terraform/services/compute/resource_compute_instance.go.tmpl index 526b562a21b7..5ee46c5a202f 100644 --- a/mmv1/third_party/terraform/services/compute/resource_compute_instance.go.tmpl +++ b/mmv1/third_party/terraform/services/compute/resource_compute_instance.go.tmpl @@ -1961,7 +1961,7 @@ func expandComputeInstance(project string, d *schema.ResourceData, config *trans DeletionProtection: d.Get("deletion_protection").(bool), Hostname: d.Get("hostname").(string), ForceSendFields: []string{"CanIpForward", "DeletionProtection"}, - AdvancedMachineFeatures: expandAdvancedMachineFeatures(d), + AdvancedMachineFeatures: expandAdvancedMachineFeaturesTyped(d), ResourcePolicies: tpgresource.ConvertStringArr(d.Get("resource_policies").([]interface{})), ReservationAffinity: reservationAffinity, KeyRevocationActionType: d.Get("key_revocation_action_type").(string), @@ -2516,7 +2516,7 @@ func populateComputeInstanceResourceData(d *schema.ResourceData, instance *compu return fmt.Errorf("Error setting confidential_instance_config: %s", err) } } - if err := d.Set("advanced_machine_features", flattenAdvancedMachineFeatures(instance.AdvancedMachineFeatures)); err != nil { + if err := d.Set("advanced_machine_features", flattenAdvancedMachineFeaturesTyped(instance.AdvancedMachineFeatures)); err != nil { return fmt.Errorf("Error setting advanced_machine_features: %s", err) } if d.Get("desired_status") != "" { @@ -4013,11 +4013,7 @@ func resourceComputeInstanceUpdate(d *schema.ResourceData, meta interface{}) err return fmt.Errorf("Error retrieving instance: %s", err) } - featuresMap, err := tpgresource.ConvertToMap(expandAdvancedMachineFeatures(d)) - if err != nil { - return fmt.Errorf("Error converting advanced_machine_features: %s", err) - } - instMap["advancedMachineFeatures"] = featuresMap + instMap["advancedMachineFeatures"] = expandAdvancedMachineFeatures(d) stripInitializeParams(instMap) diff --git a/mmv1/third_party/terraform/services/compute/resource_compute_instance_template.go.tmpl b/mmv1/third_party/terraform/services/compute/resource_compute_instance_template.go.tmpl index 9029f270710b..eb72d9af676d 100644 --- a/mmv1/third_party/terraform/services/compute/resource_compute_instance_template.go.tmpl +++ b/mmv1/third_party/terraform/services/compute/resource_compute_instance_template.go.tmpl @@ -1743,7 +1743,7 @@ func resourceComputeInstanceTemplateCreate(d *schema.ResourceData, meta interfac Scheduling: scheduling, ServiceAccounts: expandServiceAccountsTyped(d.Get("service_account").([]interface{})), Tags: tags, - AdvancedMachineFeatures: expandAdvancedMachineFeatures(d), + AdvancedMachineFeatures: expandAdvancedMachineFeaturesTyped(d), ResourcePolicies: resourcePolicies, ReservationAffinity: reservationAffinity, KeyRevocationActionType: d.Get("key_revocation_action_type").(string), @@ -2338,7 +2338,7 @@ func resourceComputeInstanceTemplateRead(d *schema.ResourceData, meta interface{ } } if instanceTemplate.Properties.AdvancedMachineFeatures != nil { - if err = d.Set("advanced_machine_features", flattenAdvancedMachineFeatures(instanceTemplate.Properties.AdvancedMachineFeatures)); err != nil { + if err = d.Set("advanced_machine_features", flattenAdvancedMachineFeaturesTyped(instanceTemplate.Properties.AdvancedMachineFeatures)); err != nil { return fmt.Errorf("Error setting advanced_machine_features: %s", err) } } diff --git a/mmv1/third_party/terraform/services/compute/resource_compute_region_instance_template.go.tmpl b/mmv1/third_party/terraform/services/compute/resource_compute_region_instance_template.go.tmpl index 1f63a596c402..af711ae36b06 100644 --- a/mmv1/third_party/terraform/services/compute/resource_compute_region_instance_template.go.tmpl +++ b/mmv1/third_party/terraform/services/compute/resource_compute_region_instance_template.go.tmpl @@ -1415,11 +1415,7 @@ func resourceComputeRegionInstanceTemplateCreate(d *schema.ResourceData, meta in instanceProperties["shieldedInstanceConfig"] = sic } if amf := expandAdvancedMachineFeatures(d); amf != nil { - amfMap, err := tpgresource.ConvertToMap(amf) - if err != nil { - return fmt.Errorf("Error converting advancedMachineFeatures: %s", err) - } - instanceProperties["advancedMachineFeatures"] = amfMap + instanceProperties["advancedMachineFeatures"] = amf } if reservationAffinity != nil { instanceProperties["reservationAffinity"] = reservationAffinity @@ -1760,7 +1756,7 @@ func resourceComputeRegionInstanceTemplateRead(d *schema.ResourceData, meta inte } } if instanceTemplate.Properties.AdvancedMachineFeatures != nil { - if err = d.Set("advanced_machine_features", flattenAdvancedMachineFeatures(instanceTemplate.Properties.AdvancedMachineFeatures)); err != nil { + if err = d.Set("advanced_machine_features", flattenAdvancedMachineFeaturesTyped(instanceTemplate.Properties.AdvancedMachineFeatures)); err != nil { return fmt.Errorf("Error setting advanced_machine_features: %s", err) } } diff --git a/mmv1/third_party/tgc/services/compute/compute_instance.go.tmpl b/mmv1/third_party/tgc/services/compute/compute_instance.go.tmpl index 2570d7859f4d..563361506d82 100644 --- a/mmv1/third_party/tgc/services/compute/compute_instance.go.tmpl +++ b/mmv1/third_party/tgc/services/compute/compute_instance.go.tmpl @@ -172,7 +172,7 @@ func expandComputeInstance(project string, d tpgresource.TerraformResourceData, DeletionProtection: d.Get("deletion_protection").(bool), Hostname: d.Get("hostname").(string), ForceSendFields: []string{"CanIpForward", "DeletionProtection"}, - AdvancedMachineFeatures: expandAdvancedMachineFeatures(d), + AdvancedMachineFeatures: expandAdvancedMachineFeaturesTyped(d), } if sicMap := expandShieldedVmConfigs(d); sicMap != nil { instance.ShieldedInstanceConfig = &compute.ShieldedInstanceConfig{ diff --git a/mmv1/third_party/tgc_next/pkg/services/compute/compute_instance_tfplan2cai.go b/mmv1/third_party/tgc_next/pkg/services/compute/compute_instance_tfplan2cai.go index 8c4fc194d333..0d2bb6a214c5 100644 --- a/mmv1/third_party/tgc_next/pkg/services/compute/compute_instance_tfplan2cai.go +++ b/mmv1/third_party/tgc_next/pkg/services/compute/compute_instance_tfplan2cai.go @@ -658,7 +658,7 @@ func expandComputeLocalSsdRecoveryTimeoutTgc(v interface{}) (*compute.Duration, } func expandAdvancedMachineFeaturesTgcNext(d tpgresource.TerraformResourceData) *compute.AdvancedMachineFeatures { - features := expandAdvancedMachineFeatures(d) + features := expandAdvancedMachineFeaturesTyped(d) if features != nil && features.PerformanceMonitoringUnit == "" { features.PerformanceMonitoringUnit = "STANDARD" } From 3f2e48b2e10b50876f75074ef6ae5dc14e435314 Mon Sep 17 00:00:00 2001 From: guvenenb Date: Mon, 29 Jun 2026 21:23:49 +0200 Subject: [PATCH 10/78] Revert "feat(vertexai): add google_vertex_ai_online_evaluator resource" (#18084) --- .github/workflows/unit-test-mmv1.yml | 2 +- mmv1/products/vertexai/OnlineEvaluator.yaml | 200 ------------------ .../vertex_ai_online_evaluator_basic.tf.tmpl | 71 ------- ...ce_vertex_ai_online_evaluator_test.go.tmpl | 180 ---------------- .../issue-labeler/labeler/enrolled_teams.yml | 1 - 5 files changed, 1 insertion(+), 453 deletions(-) delete mode 100644 mmv1/products/vertexai/OnlineEvaluator.yaml delete mode 100644 mmv1/templates/terraform/samples/services/vertexai/vertex_ai_online_evaluator_basic.tf.tmpl delete mode 100644 mmv1/third_party/terraform/services/vertexai/resource_vertex_ai_online_evaluator_test.go.tmpl diff --git a/.github/workflows/unit-test-mmv1.yml b/.github/workflows/unit-test-mmv1.yml index ddcda7b69649..0621df306c20 100644 --- a/.github/workflows/unit-test-mmv1.yml +++ b/.github/workflows/unit-test-mmv1.yml @@ -90,7 +90,7 @@ jobs: cd repo if [ "${{ github.event_name }}" == "pull_request" ]; then # For PRs, get only changed files - yamlfiles=$(git diff --name-only origin/${GITHUB_BASE_REF} -- mmv1/products) + yamlfiles=$(git diff --name-only --diff-filter=d origin/${GITHUB_BASE_REF} -- mmv1/products) if [ ! -z "$yamlfiles" ]; then echo "yamlfiles=${yamlfiles//$'\n'/ }" >> $GITHUB_OUTPUT fi diff --git a/mmv1/products/vertexai/OnlineEvaluator.yaml b/mmv1/products/vertexai/OnlineEvaluator.yaml deleted file mode 100644 index 671d2655c6ee..000000000000 --- a/mmv1/products/vertexai/OnlineEvaluator.yaml +++ /dev/null @@ -1,200 +0,0 @@ ---- -# 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: OnlineEvaluator -description: Description -base_url: projects/{{project}}/locations/{{region}}/onlineEvaluators -min_version: beta -self_link: projects/{{project}}/locations/{{region}}/onlineEvaluators/{{name}} -create_url: projects/{{project}}/locations/{{region}}/onlineEvaluators -update_mask: true -update_verb: PATCH -timeouts: - insert_minutes: 20 - update_minutes: 20 - delete_minutes: 60 -async: - type: OpAsync - operation: - base_url: '{{op_id}}' - actions: - - create - - update - - delete - result: - resource_inside_response: true -autogen_status: T25saW5lRXZhbHVhdG9y -samples: - - name: vertex_ai_online_evaluator_basic - primary_resource_id: evaluator - steps: - - name: vertex_ai_online_evaluator_basic - resource_id_vars: - evaluator_name: my-evaluator - engine_name: my-engine -parameters: - - name: region - type: String - required: true - 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 -properties: - - name: agentResource - type: String - required: true - description: The name of the agent that the OnlineEvaluator evaluates periodically. This value is used to filter the traces with a matching cloud.resource_id and link the evaluation results with relevant dashboards/UIs. This field is immutable. Once set, it cannot be changed. - immutable: true - - name: cloudObservability - type: NestedObject - description: Data source for the OnlineEvaluator, based on Google Cloud Observability stack (Cloud Trace & Cloud Logging). - properties: - - name: logView - type: String - description: Optional log view that will be used to query logs. If empty, the `_Default` view will be used. - immutable: true - - name: openTelemetry - type: NestedObject - description: Configuration for data source following OpenTelemetry. - immutable: true - ignore_read: true - properties: - - name: semconvVersion - type: String - required: true - description: Defines which version OTel Semantic Convention the data follows. Can be "1.39.0" or newer. - immutable: true - - name: traceScope - type: NestedObject - description: If chosen, the online evaluator will evaluate single traces matching specified `filter`. - immutable: true - properties: - - name: filter - type: Array - description: A list of predicates to filter traces. Multiple predicates are combined using AND. The maximum number of predicates is 10. - immutable: true - item_type: - type: NestedObject - properties: - - name: duration - type: NestedObject - description: Defines a predicate for filtering based on a numeric value. - immutable: true - properties: - - name: comparisonOperator - type: String - required: true - description: 'The comparison operator to apply. Possible values: LESS LESS_OR_EQUAL EQUAL NOT_EQUAL GREATER_OR_EQUAL GREATER' - immutable: true - - name: value - type: Double - required: true - description: The value to compare against. - immutable: true - - name: totalTokenUsage - type: NestedObject - description: Defines a predicate for filtering based on a numeric value. - immutable: true - properties: - - name: comparisonOperator - type: String - required: true - description: 'The comparison operator to apply. Possible values: LESS LESS_OR_EQUAL EQUAL NOT_EQUAL GREATER_OR_EQUAL GREATER' - immutable: true - - name: value - type: Double - required: true - description: The value to compare against. - immutable: true - - name: traceView - type: String - description: 'Optional trace view that will be used to query traces. If empty, the `_Default` view will be used. NOTE: This field is not supported yet and will be ignored if set.' - immutable: true - required: true - immutable: true - - name: config - type: NestedObject - required: true - description: Configuration for sampling behavior of the OnlineEvaluator. The OnlineEvaluator runs at a fixed interval of 10 minutes. - properties: - - name: maxEvaluatedSamplesPerRun - type: String - description: The maximum number of evaluations to perform per run. If set to 0, the number is unbounded. - - name: randomSampling - type: NestedObject - description: Configuration for random sampling. - properties: - - name: percentage - type: Integer - required: true - description: The percentage of traces to sample for evaluation. Must be an integer between `1` and `100`. - - name: createTime - type: String - description: Timestamp when the OnlineEvaluator was created. - output: true - - name: displayName - type: String - description: Human-readable name for the `OnlineEvaluator`. The name doesn't have to be unique. The name can consist of any UTF-8 characters. The maximum length is `63` characters. If the display name exceeds max characters, an `INVALID_ARGUMENT` error is returned. - - name: metricSources - type: Array - required: true - immutable: true - description: A list of metric sources to be used for evaluating samples. At least one MetricSource must be provided. Right now, only predefined metrics and registered metrics are supported. Every registered metric must have `display_name` (or `title`) and `score_range` defined. Otherwise, the evaluations will fail. The maximum number of `metric_sources` is 25. - item_type: - type: NestedObject - properties: - # TODO: Support structured metric config instead of JSON string. - - name: metric - type: String - description: | - Inline metric config. Provide this field as a JSON-formatted string. - Suggested predefined metricSpecName values: - - `final_response_quality_v1` - - `tool_use_quality_v1` - - `hallucination_v1` - - `safety_v1` - - `multi_turn_task_success_v1` - - `multi_turn_tool_use_quality_v1` - - `multi_turn_trajectory_quality_v1` - immutable: true - custom_expand: templates/terraform/custom_expand/json_value.tmpl - custom_flatten: templates/terraform/custom_flatten/json_schema.tmpl - - name: metricResourceName - type: String - description: Resource name for registered metric. - immutable: true - - name: name - type: String - description: 'Identifier. The resource name of the OnlineEvaluator. Format: projects/{project}/locations/{region}/onlineEvaluators/{id}.' - output: true - custom_flatten: templates/terraform/custom_flatten/name_from_self_link.tmpl - - name: state - type: String - description: 'The state of the OnlineEvaluator. Possible values: ACTIVE SUSPENDED FAILED WARNING' - output: true - - name: stateDetails - type: Array - description: Contains additional information about the state of the OnlineEvaluator. This is used to provide more details in the event of a failure. - output: true - item_type: - type: NestedObject - output: true - properties: - - name: message - type: String - description: Human-readable message describing the state of the OnlineEvaluator. - output: true - - name: updateTime - type: String - description: Timestamp when the OnlineEvaluator was last updated. - output: true diff --git a/mmv1/templates/terraform/samples/services/vertexai/vertex_ai_online_evaluator_basic.tf.tmpl b/mmv1/templates/terraform/samples/services/vertexai/vertex_ai_online_evaluator_basic.tf.tmpl deleted file mode 100644 index 137cce667b7d..000000000000 --- a/mmv1/templates/terraform/samples/services/vertexai/vertex_ai_online_evaluator_basic.tf.tmpl +++ /dev/null @@ -1,71 +0,0 @@ -resource "google_vertex_ai_reasoning_engine" "engine" { - provider = google-beta - display_name = "{{index $.ResourceIdVars "engine_name"}}" - description = "A basic reasoning engine" - labels = { - "key" = "value" - } - region = "us-central1" -} - -resource "google_vertex_ai_online_evaluator" "evaluator" { - provider = google-beta - region = "us-central1" - display_name = "{{index $.ResourceIdVars "evaluator_name"}}" - - agent_resource = google_vertex_ai_reasoning_engine.engine.id - - config { - max_evaluated_samples_per_run = "100" - random_sampling { - percentage = 10 - } - } - - metric_sources { - metric = jsonencode({ - "predefinedMetricSpec" = { - "metricSpecName" = "safety_v1" - } - }) - } - - metric_sources { - metric = jsonencode({ - "predefinedMetricSpec" = { - "metricSpecName" = "hallucination_v1" - } - }) - } - - metric_sources { - metric = jsonencode({ - "predefinedMetricSpec" = { - "metricSpecName" = "final_response_quality_v1" - } - }) - } - - metric_sources { - metric = jsonencode({ - "predefinedMetricSpec" = { - "metricSpecName" = "tool_use_quality_v1" - } - }) - } - - cloud_observability { - open_telemetry { - semconv_version = "1.39.0" - } - - trace_scope { - filter { - duration { - comparison_operator = "GREATER" - value = 0 - } - } - } - } -} diff --git a/mmv1/third_party/terraform/services/vertexai/resource_vertex_ai_online_evaluator_test.go.tmpl b/mmv1/third_party/terraform/services/vertexai/resource_vertex_ai_online_evaluator_test.go.tmpl deleted file mode 100644 index 74425e4914e9..000000000000 --- a/mmv1/third_party/terraform/services/vertexai/resource_vertex_ai_online_evaluator_test.go.tmpl +++ /dev/null @@ -1,180 +0,0 @@ -package vertexai_test - -{{ if ne $.TargetVersionName "ga" -}} -import ( - "testing" - - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-provider-google/google/acctest" -) - -func TestAccVertexAIOnlineEvaluator_update(t *testing.T) { - t.Parallel() - - randomSuffix := acctest.RandString(t, 10) - - context := map[string]interface{}{ - "random_suffix": randomSuffix, - } - - acctest.VcrTest(t, resource.TestCase{ - PreCheck: func() { acctest.AccTestPreCheck(t) }, - ProtoV5ProviderFactories: acctest.ProtoV5ProviderBetaFactories(t), - CheckDestroy: testAccCheckVertexAIOnlineEvaluatorDestroyProducer(t), - Steps: []resource.TestStep{ - { - Config: testAccVertexAIOnlineEvaluator_basic(context), - }, - { - ResourceName: "google_vertex_ai_online_evaluator.evaluator", - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"region", "project", "cloud_observability.0.open_telemetry"}, - }, - { - Config: testAccVertexAIOnlineEvaluator_update(context), - }, - { - ResourceName: "google_vertex_ai_online_evaluator.evaluator", - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"region", "project", "cloud_observability.0.open_telemetry"}, - }, - }, - }) -} - -func testAccVertexAIOnlineEvaluator_basic(context map[string]interface{}) string { - return acctest.Nprintf(` -resource "google_vertex_ai_reasoning_engine" "engine" { - provider = google-beta - display_name = "tf-test-re-%{random_suffix}" - description = "A basic reasoning engine" - labels = { - "key" = "value" - } - region = "us-central1" -} - -resource "google_vertex_ai_online_evaluator" "evaluator" { - provider = google-beta - region = "us-central1" - display_name = "tf-test-eval-%{random_suffix}" - - agent_resource = google_vertex_ai_reasoning_engine.engine.id - - config { - max_evaluated_samples_per_run = "100" - random_sampling { - percentage = 10 - } - } - - metric_sources { - metric = jsonencode({ - "predefinedMetricSpec" = { - "metricSpecName" = "safety_v1" - } - }) - } - - metric_sources { - metric = jsonencode({ - "predefinedMetricSpec" = { - "metricSpecName" = "hallucination_v1" - } - }) - } - - cloud_observability { - open_telemetry { - semconv_version = "1.39.0" - } - - trace_scope { - filter { - duration { - comparison_operator = "GREATER" - value = 0 - } - } - filter { - total_token_usage { - comparison_operator = "LESS" - value = 1000 - } - } - } - } -} -`, context) -} - -func testAccVertexAIOnlineEvaluator_update(context map[string]interface{}) string { - return acctest.Nprintf(` -resource "google_vertex_ai_reasoning_engine" "engine" { - provider = google-beta - display_name = "tf-test-re-%{random_suffix}" - description = "A basic reasoning engine" - labels = { - "key" = "value" - } - region = "us-central1" -} - -resource "google_vertex_ai_online_evaluator" "evaluator" { - provider = google-beta - region = "us-central1" - display_name = "tf-test-eval-upd-%{random_suffix}" - - agent_resource = google_vertex_ai_reasoning_engine.engine.id - - config { - max_evaluated_samples_per_run = "200" - random_sampling { - percentage = 20 - } - } - - metric_sources { - metric = jsonencode({ - "predefinedMetricSpec" = { - "metricSpecName" = "safety_v1" - } - }) - } - - metric_sources { - metric = jsonencode({ - "predefinedMetricSpec" = { - "metricSpecName" = "hallucination_v1" - } - }) - } - - cloud_observability { - open_telemetry { - semconv_version = "1.39.0" - } - - trace_scope { - filter { - duration { - comparison_operator = "GREATER" - value = 0 - } - } - filter { - total_token_usage { - comparison_operator = "LESS" - value = 1000 - } - } - } - } -} -`, context) -} - - -{{- end }} diff --git a/tools/issue-labeler/labeler/enrolled_teams.yml b/tools/issue-labeler/labeler/enrolled_teams.yml index c6502889b1f0..3718120f54a7 100755 --- a/tools/issue-labeler/labeler/enrolled_teams.yml +++ b/tools/issue-labeler/labeler/enrolled_teams.yml @@ -10,7 +10,6 @@ service/accesscontextmanager: service/aiplatform-agent-engine: resources: - google_vertex_ai_reasoning_engine.* - - google_vertex_ai_online_evaluator service/aiplatform-colab-enterprise: resources: - google_colab_.* From fea0fa59fb04c70046e9d11909d7ed26591deb3b Mon Sep 17 00:00:00 2001 From: Elijah Date: Mon, 29 Jun 2026 16:48:36 -0400 Subject: [PATCH 11/78] retry Cloud Scheduler 409 sync mutate cannot be queued (#17750) --- mmv1/products/cloudscheduler/Job.yaml | 2 ++ .../resource_cloud_scheduler_job_test.go | 33 +++++++++++++++++++ .../transport/error_retry_predicates.go | 12 +++++++ 3 files changed, 47 insertions(+) diff --git a/mmv1/products/cloudscheduler/Job.yaml b/mmv1/products/cloudscheduler/Job.yaml index 7f3d9fdf29e5..e6d81a151394 100644 --- a/mmv1/products/cloudscheduler/Job.yaml +++ b/mmv1/products/cloudscheduler/Job.yaml @@ -29,6 +29,8 @@ timeouts: insert_minutes: 20 update_minutes: 20 delete_minutes: 20 +error_retry_predicates: + - transport_tpg.Is409SyncMutateCannotBeQueuedError custom_code: constants: 'templates/terraform/constants/scheduler.tmpl' encoder: 'templates/terraform/encoders/cloud_scheduler.go.tmpl' diff --git a/mmv1/third_party/terraform/services/cloudscheduler/resource_cloud_scheduler_job_test.go b/mmv1/third_party/terraform/services/cloudscheduler/resource_cloud_scheduler_job_test.go index 63b4778de33e..f32a9ed743a4 100644 --- a/mmv1/third_party/terraform/services/cloudscheduler/resource_cloud_scheduler_job_test.go +++ b/mmv1/third_party/terraform/services/cloudscheduler/resource_cloud_scheduler_job_test.go @@ -1,11 +1,13 @@ package cloudscheduler_test import ( + "fmt" "testing" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-provider-google/google/acctest" "github.com/hashicorp/terraform-provider-google/google/services/cloudscheduler" + transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" ) func TestAccCloudSchedulerJob_schedulerPausedExample(t *testing.T) { @@ -124,3 +126,34 @@ resource "google_cloud_scheduler_job" "job" { } `, context) } + +func TestUnitCloudSchedulerJob_Is409SyncMutateCannotBeQueuedError(t *testing.T) { + cases := map[string]struct { + Err error + Expected bool + }{ + "sync mutate cannot be queued error": { + Err: fmt.Errorf("googleapi: Error 409: sync mutate calls cannot be queued"), + Expected: true, + }, + "operation in progress error": { + Err: fmt.Errorf("googleapi: Error 409: operationInProgress"), + Expected: false, + }, + "generic error": { + Err: fmt.Errorf("some generic error"), + Expected: false, + }, + "nil error": { + Err: nil, + Expected: false, + }, + } + + for tn, tc := range cases { + retry, _ := transport_tpg.Is409SyncMutateCannotBeQueuedError(tc.Err) + if retry != tc.Expected { + t.Fatalf("bad: %s, expected %t got %t", tn, tc.Expected, retry) + } + } +} diff --git a/mmv1/third_party/terraform/transport/error_retry_predicates.go b/mmv1/third_party/terraform/transport/error_retry_predicates.go index b26bbecac691..55ce243d6368 100644 --- a/mmv1/third_party/terraform/transport/error_retry_predicates.go +++ b/mmv1/third_party/terraform/transport/error_retry_predicates.go @@ -712,3 +712,15 @@ func IsDataplex1PEntryNotFoundError(err error) (bool, string) { } return false, "" } + +// Retry on Cloud Scheduler 'Sync Mutate Cannot Be Queued' +func Is409SyncMutateCannotBeQueuedError(err error) (bool, string) { + if err == nil { + return false, "" + } + errStr := err.Error() + if strings.Contains(errStr, "Error 409") && strings.Contains(errStr, "sync mutate calls cannot be queued") { + return true, "sync mutate calls cannot be queued" + } + return false, "" +} From 109ce502a0b165f79677ef5467c42fffa73381fc Mon Sep 17 00:00:00 2001 From: chayan kumar roy Date: Tue, 30 Jun 2026 20:53:40 +0200 Subject: [PATCH 12/78] =?UTF-8?q?feat(compute):=20Promote=20instanceLifecy?= =?UTF-8?q?clePolicy.onRepair.allowChanging=E2=80=A6=20(#18104)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...rce_compute_instance_group_manager.go.tmpl | 14 ------------- ...pute_instance_group_manager_meta.yaml.tmpl | 2 -- ...ompute_instance_group_manager_test.go.tmpl | 6 ------ ...pute_region_instance_group_manager.go.tmpl | 4 ---- ...gion_instance_group_manager_meta.yaml.tmpl | 2 -- ...region_instance_group_manager_test.go.tmpl | 20 ------------------- ...mpute_instance_group_manager.html.markdown | 4 ++-- ...egion_instance_group_manager.html.markdown | 4 ++-- 8 files changed, 4 insertions(+), 52 deletions(-) diff --git a/mmv1/third_party/terraform/services/compute/resource_compute_instance_group_manager.go.tmpl b/mmv1/third_party/terraform/services/compute/resource_compute_instance_group_manager.go.tmpl index cffd60747f62..42ae9c3f2149 100644 --- a/mmv1/third_party/terraform/services/compute/resource_compute_instance_group_manager.go.tmpl +++ b/mmv1/third_party/terraform/services/compute/resource_compute_instance_group_manager.go.tmpl @@ -378,7 +378,6 @@ func ResourceComputeInstanceGroupManager() *schema.Resource { Description: `Specifies whether to apply the group's latest configuration when repairing a VM. Valid options are: YES, NO. If YES and you updated the group's instance template or per-instance configurations after the VM was created, then these changes are applied when VM is repaired. If NO (default), then updates are applied in accordance with the group's update policy type.`, }, - {{- if ne $.TargetVersionName `ga` }} "on_repair": { Type: schema.TypeList, Computed: true, @@ -397,7 +396,6 @@ func ResourceComputeInstanceGroupManager() *schema.Resource { }, }, }, - {{- end }} }, }, @@ -1822,9 +1820,7 @@ func expandInstanceLifecyclePolicy(configured []interface{}) *compute.InstanceGr instanceLifecyclePolicy.DefaultActionOnFailure = data["default_action_on_failure"].(string) instanceLifecyclePolicy.OnFailedHealthCheck = data["on_failed_health_check"].(string) - {{- if ne $.TargetVersionName `ga` }} instanceLifecyclePolicy.OnRepair = expandOnRepair(data["on_repair"].([]any)) - {{- end }} } return instanceLifecyclePolicy } @@ -1837,14 +1833,11 @@ func expandInstanceLifecyclePolicyV2(configured []interface{}) map[string]interf instanceLifecyclePolicy["defaultActionOnFailure"] = data["default_action_on_failure"].(string) instanceLifecyclePolicy["onFailedHealthCheck"] = data["on_failed_health_check"].(string) - {{- if ne $.TargetVersionName `ga` }} instanceLifecyclePolicy["onRepair"] = expandOnRepairV2(data["on_repair"].([]any)) - {{- end }} } return instanceLifecyclePolicy } -{{ if ne $.TargetVersionName `ga` -}} func expandOnRepair(configured []any) *compute.InstanceGroupManagerInstanceLifecyclePolicyOnRepair { onRepair := &compute.InstanceGroupManagerInstanceLifecyclePolicyOnRepair{} for _, raw := range configured { @@ -1862,7 +1855,6 @@ func expandOnRepairV2(configured []any) map[string]interface{} { } return onRepair } -{{- end }} func expandStandbyPolicy(d *schema.ResourceData) *compute.InstanceGroupManagerStandbyPolicy { standbyPolicy := &compute.InstanceGroupManagerStandbyPolicy{} @@ -2124,9 +2116,7 @@ func flattenInstanceLifecyclePolicy(instanceLifecyclePolicy *compute.InstanceGro ilp["default_action_on_failure"] = instanceLifecyclePolicy.DefaultActionOnFailure ilp["on_failed_health_check"] = instanceLifecyclePolicy.OnFailedHealthCheck - {{ if ne $.TargetVersionName `ga` -}} ilp["on_repair"] = flattenOnRepair(instanceLifecyclePolicy.OnRepair) - {{- end }} results = append(results, ilp) } return results @@ -2141,15 +2131,12 @@ func flattenInstanceLifecyclePolicyV2(raw interface{}) []map[string]interface{} ilp["default_action_on_failure"] = stringFromMap(instanceLifecyclePolicy, "defaultActionOnFailure") ilp["on_failed_health_check"] = stringFromMap(instanceLifecyclePolicy, "onFailedHealthCheck") - {{ if ne $.TargetVersionName `ga` -}} ilp["on_repair"] = flattenOnRepairV2(instanceLifecyclePolicy["onRepair"]) - {{- end }} results = append(results, ilp) } return results } -{{ if ne $.TargetVersionName `ga` -}} func flattenOnRepair(onRepair *compute.InstanceGroupManagerInstanceLifecyclePolicyOnRepair) []map[string]any { results := []map[string]any{} if onRepair != nil { @@ -2170,7 +2157,6 @@ func flattenOnRepairV2(raw interface{}) []map[string]any { } return results } -{{- end }} func expandAllInstancesConfig(old []interface{}, new []interface{}) *compute.InstanceGroupManagerAllInstancesConfig { var properties *compute.InstancePropertiesPatch diff --git a/mmv1/third_party/terraform/services/compute/resource_compute_instance_group_manager_meta.yaml.tmpl b/mmv1/third_party/terraform/services/compute/resource_compute_instance_group_manager_meta.yaml.tmpl index 437f244ccf2d..bfcff2a81fb1 100644 --- a/mmv1/third_party/terraform/services/compute/resource_compute_instance_group_manager_meta.yaml.tmpl +++ b/mmv1/third_party/terraform/services/compute/resource_compute_instance_group_manager_meta.yaml.tmpl @@ -26,9 +26,7 @@ fields: - api_field: 'instanceLifecyclePolicy.defaultActionOnFailure' - api_field: 'instanceLifecyclePolicy.onFailedHealthCheck' - api_field: 'instanceLifecyclePolicy.forceUpdateOnRepair' -{{- if ne $.TargetVersionName "ga" }} - api_field: 'instanceLifecyclePolicy.onRepair.allowChangingZone' -{{- end }} - api_field: 'listManagedInstancesResults' - api_field: 'name' - api_field: 'namedPorts.name' diff --git a/mmv1/third_party/terraform/services/compute/resource_compute_instance_group_manager_test.go.tmpl b/mmv1/third_party/terraform/services/compute/resource_compute_instance_group_manager_test.go.tmpl index b3ea7f7da445..965dd9cced2e 100644 --- a/mmv1/third_party/terraform/services/compute/resource_compute_instance_group_manager_test.go.tmpl +++ b/mmv1/third_party/terraform/services/compute/resource_compute_instance_group_manager_test.go.tmpl @@ -176,9 +176,7 @@ func TestAccInstanceGroupManager_update(t *testing.T) { resource.TestCheckResourceAttr("google_compute_instance_group_manager.igm-update", "instance_lifecycle_policy.0.default_action_on_failure", "DO_NOTHING"), resource.TestCheckResourceAttr("google_compute_instance_group_manager.igm-update", "instance_lifecycle_policy.0.on_failed_health_check", "DO_NOTHING"), - {{- if ne $.TargetVersionName "ga" }} resource.TestCheckResourceAttr("google_compute_instance_group_manager.igm-update", "instance_lifecycle_policy.0.on_repair.0.allow_changing_zone", "NO"), - {{- end }} ), }, { @@ -193,9 +191,7 @@ func TestAccInstanceGroupManager_update(t *testing.T) { resource.TestCheckResourceAttr("google_compute_instance_group_manager.igm-update", "instance_lifecycle_policy.0.default_action_on_failure", "REPAIR"), resource.TestCheckResourceAttr("google_compute_instance_group_manager.igm-update", "instance_lifecycle_policy.0.on_failed_health_check", "REPAIR"), - {{- if ne $.TargetVersionName "ga" }} resource.TestCheckResourceAttr("google_compute_instance_group_manager.igm-update", "instance_lifecycle_policy.0.on_repair.0.allow_changing_zone", "NO"), - {{- end }} ), }, { @@ -995,11 +991,9 @@ resource "google_compute_instance_group_manager" "igm-update" { default_action_on_failure = "REPAIR" on_failed_health_check = "REPAIR" - {{- if ne $.TargetVersionName "ga" }} on_repair { allow_changing_zone = "NO" } - {{- end }} } } `, template1, target1, target2, template2, description, igm) diff --git a/mmv1/third_party/terraform/services/compute/resource_compute_region_instance_group_manager.go.tmpl b/mmv1/third_party/terraform/services/compute/resource_compute_region_instance_group_manager.go.tmpl index 2ebbf974c825..1fcbe3f014aa 100644 --- a/mmv1/third_party/terraform/services/compute/resource_compute_region_instance_group_manager.go.tmpl +++ b/mmv1/third_party/terraform/services/compute/resource_compute_region_instance_group_manager.go.tmpl @@ -602,7 +602,6 @@ func ResourceComputeRegionInstanceGroupManager() *schema.Resource { Description: `Specifies whether to apply the group's latest configuration when repairing a VM. Valid options are: YES, NO. If YES and you updated the group's instance template or per-instance configurations after the VM was created, then these changes are applied when VM is repaired. If NO (default), then updates are applied in accordance with the group's update policy type.`, }, - {{- if ne $.TargetVersionName `ga` }} "on_repair": { Type: schema.TypeList, Computed: true, @@ -621,7 +620,6 @@ func ResourceComputeRegionInstanceGroupManager() *schema.Resource { }, }, }, - {{- end }} }, }, @@ -2258,12 +2256,10 @@ func flattenRegionInstanceLifecyclePolicy(raw interface{}) []map[string]interfac "default_action_on_failure": ilp["defaultActionOnFailure"], "on_failed_health_check": ilp["onFailedHealthCheck"], } -{{- if ne $.TargetVersionName "ga" }} if onRepair, ok := ilp["onRepair"].(map[string]interface{}); ok && onRepair != nil { onRepairEntry := map[string]any{"allow_changing_zone": onRepair["allowChangingZone"]} entry["on_repair"] = []map[string]any{onRepairEntry} } -{{- end }} return append(results, entry) } diff --git a/mmv1/third_party/terraform/services/compute/resource_compute_region_instance_group_manager_meta.yaml.tmpl b/mmv1/third_party/terraform/services/compute/resource_compute_region_instance_group_manager_meta.yaml.tmpl index 6d5827ad3f8e..b2fbecaef4cc 100644 --- a/mmv1/third_party/terraform/services/compute/resource_compute_region_instance_group_manager_meta.yaml.tmpl +++ b/mmv1/third_party/terraform/services/compute/resource_compute_region_instance_group_manager_meta.yaml.tmpl @@ -110,9 +110,7 @@ fields: - api_field: 'instanceLifecyclePolicy.defaultActionOnFailure' - api_field: 'instanceLifecyclePolicy.onFailedHealthCheck' - api_field: 'instanceLifecyclePolicy.forceUpdateOnRepair' -{{- if ne $.TargetVersionName "ga" }} - api_field: 'instanceLifecyclePolicy.onRepair.allowChangingZone' -{{- end }} - api_field: 'listManagedInstancesResults' - api_field: 'name' - api_field: 'namedPorts.name' diff --git a/mmv1/third_party/terraform/services/compute/resource_compute_region_instance_group_manager_test.go.tmpl b/mmv1/third_party/terraform/services/compute/resource_compute_region_instance_group_manager_test.go.tmpl index b2477ee66495..718bf9bbfcd2 100644 --- a/mmv1/third_party/terraform/services/compute/resource_compute_region_instance_group_manager_test.go.tmpl +++ b/mmv1/third_party/terraform/services/compute/resource_compute_region_instance_group_manager_test.go.tmpl @@ -95,11 +95,8 @@ func TestAccRegionInstanceGroupManager_update(t *testing.T) { resource.TestCheckResourceAttr("google_compute_region_instance_group_manager.igm-update", "instance_lifecycle_policy.0.default_action_on_failure", "DO_NOTHING"), resource.TestCheckResourceAttr("google_compute_region_instance_group_manager.igm-update", "instance_lifecycle_policy.0.on_failed_health_check", "DO_NOTHING"), - {{- if ne $.TargetVersionName "ga" }} resource.TestCheckResourceAttr("google_compute_region_instance_group_manager.igm-update", "instance_lifecycle_policy.0.force_update_on_repair", "NO"), resource.TestCheckResourceAttr("google_compute_region_instance_group_manager.igm-update", "instance_lifecycle_policy.0.on_repair.0.allow_changing_zone", "NO"), - {{- end }} - ), }, { @@ -114,11 +111,8 @@ func TestAccRegionInstanceGroupManager_update(t *testing.T) { resource.TestCheckResourceAttr("google_compute_region_instance_group_manager.igm-update", "instance_lifecycle_policy.0.default_action_on_failure", "REPAIR"), resource.TestCheckResourceAttr("google_compute_region_instance_group_manager.igm-update", "instance_lifecycle_policy.0.on_failed_health_check", "REPAIR"), - {{- if ne $.TargetVersionName "ga" }} resource.TestCheckResourceAttr("google_compute_region_instance_group_manager.igm-update", "instance_lifecycle_policy.0.force_update_on_repair", "YES"), resource.TestCheckResourceAttr("google_compute_region_instance_group_manager.igm-update", "instance_lifecycle_policy.0.on_repair.0.allow_changing_zone", "YES"), - {{- end }} - ), }, { @@ -133,11 +127,8 @@ func TestAccRegionInstanceGroupManager_update(t *testing.T) { resource.TestCheckResourceAttr("google_compute_region_instance_group_manager.igm-update", "instance_lifecycle_policy.0.default_action_on_failure", "REPAIR"), resource.TestCheckResourceAttr("google_compute_region_instance_group_manager.igm-update", "instance_lifecycle_policy.0.on_failed_health_check", "REPAIR"), - {{- if ne $.TargetVersionName "ga" }} resource.TestCheckResourceAttr("google_compute_region_instance_group_manager.igm-update", "instance_lifecycle_policy.0.force_update_on_repair", "YES"), resource.TestCheckResourceAttr("google_compute_region_instance_group_manager.igm-update", "instance_lifecycle_policy.0.on_repair.0.allow_changing_zone", "YES"), - {{- end }} - ), }, { @@ -803,11 +794,7 @@ resource "google_compute_region_instance_group_manager" "igm-update" { } instance_lifecycle_policy { - {{- if ne $.TargetVersionName "ga" }} force_update_on_repair = "NO" - {{- else }} - force_update_on_repair = "YES" - {{- end }} default_action_on_failure = "DO_NOTHING" on_failed_health_check = "DO_NOTHING" @@ -920,17 +907,10 @@ resource "google_compute_region_instance_group_manager" "igm-update" { default_action_on_failure = "REPAIR" on_failed_health_check = "REPAIR" - {{- if ne $.TargetVersionName "ga" }} force_update_on_repair = "YES" - {{- else }} - force_update_on_repair = "NO" - {{- end }} - - {{- if ne $.TargetVersionName "ga" }} on_repair { allow_changing_zone = "YES" } - {{- end }} } } diff --git a/mmv1/third_party/terraform/website/docs/r/compute_instance_group_manager.html.markdown b/mmv1/third_party/terraform/website/docs/r/compute_instance_group_manager.html.markdown index 42acbf2e6668..4f217ccca5ef 100644 --- a/mmv1/third_party/terraform/website/docs/r/compute_instance_group_manager.html.markdown +++ b/mmv1/third_party/terraform/website/docs/r/compute_instance_group_manager.html.markdown @@ -313,7 +313,7 @@ instance_lifecycle_policy { force_update_on_repair = "YES" default_action_on_failure = "DO_NOTHING" on_failed_health_check = "DO_NOTHING" - on_repair { //google-beta only + on_repair { allow_changing_zone = "YES" } } @@ -322,7 +322,7 @@ instance_lifecycle_policy { * `force_update_on_repair` - (Optional), Specifies whether to apply the group's latest configuration when repairing a VM. Valid options are: `YES`, `NO`. If `YES` and you updated the group's instance template or per-instance configurations after the VM was created, then these changes are applied when VM is repaired. If `NO` (default), then updates are applied in accordance with the group's update policy type. * `default_action_on_failure` - (Optional), Specifies the action that a MIG performs on a failed VM. If the value of the `on_failed_health_check` field is `DEFAULT_ACTION`, then the same action also applies to the VMs on which your application fails a health check. Valid options are: `DO_NOTHING`, `REPAIR`. If `DO_NOTHING`, then MIG does not repair a failed VM. If `REPAIR` (default), then MIG automatically repairs a failed VM by recreating it. For more information, see about repairing VMs in a MIG. * `on_failed_health_check` - (Optional), Specifies the action that a MIG performs on an unhealthy VM. A VM is marked as unhealthy when the application running on that VM fails a health check. Valid options are: `DEFAULT_ACTION`, `DO_NOTHING`, `REPAIR`. If `DEFAULT_ACTION` (default), then MIG uses the same action configured for the `default_action_on_failure` field. If `DO_NOTHING`, then MIG does not repair unhealthy VM. If `REPAIR`, then MIG automatically repairs an unhealthy VM by recreating it. For more information, see about repairing VMs in a MIG. -* `on_repair` - (Optional, [Beta](../guides/provider_versions.html.markdown)), Configuration for VM repairs in the MIG. Structure is [documented below](#nested_on_repair). +* `on_repair` - (Optional), Configuration for VM repairs in the MIG. Structure is [documented below](#nested_on_repair). - - - The `on_repair` block supports: diff --git a/mmv1/third_party/terraform/website/docs/r/compute_region_instance_group_manager.html.markdown b/mmv1/third_party/terraform/website/docs/r/compute_region_instance_group_manager.html.markdown index 86ec2740d800..9cac3f0d0f01 100644 --- a/mmv1/third_party/terraform/website/docs/r/compute_region_instance_group_manager.html.markdown +++ b/mmv1/third_party/terraform/website/docs/r/compute_region_instance_group_manager.html.markdown @@ -268,7 +268,7 @@ instance_lifecycle_policy { force_update_on_repair = "YES" default_action_on_failure = "DO_NOTHING" on_failed_health_check = "DO_NOTHING" - on_repair { //google-beta only + on_repair { allow_changing_zone = "YES" } } @@ -277,7 +277,7 @@ instance_lifecycle_policy { * `force_update_on_repair` - (Optional), Specifies whether to apply the group's latest configuration when repairing a VM. Valid options are: `YES`, `NO`. If `YES` and you updated the group's instance template or per-instance configurations after the VM was created, then these changes are applied when VM is repaired. If `NO` (default), then updates are applied in accordance with the group's update policy type. * `default_action_on_failure` - (Optional), Specifies the action that a MIG performs on a failed VM. If the value of the `on_failed_health_check` field is `DEFAULT_ACTION`, then the same action also applies to the VMs on which your application fails a health check. Valid options are: `DO_NOTHING`, `REPAIR`. If `DO_NOTHING`, then MIG does not repair a failed VM. If `REPAIR` (default), then MIG automatically repairs a failed VM by recreating it. For more information, see about repairing VMs in a MIG. * `on_failed_health_check` - (Optional), Specifies the action that a MIG performs on an unhealthy VM. A VM is marked as unhealthy when the application running on that VM fails a health check. Valid options are: `DEFAULT_ACTION`, `DO_NOTHING`, `REPAIR`. If `DEFAULT_ACTION` (default), then MIG uses the same action configured for the `default_action_on_failure` field. If `DO_NOTHING`, then MIG does not repair unhealthy VM. If `REPAIR`, then MIG automatically repairs an unhealthy VM by recreating it. For more information, see about repairing VMs in a MIG. -* `on_repair` - (Optional, [Beta](../guides/provider_versions.html.markdown)), Configuration for VM repairs in the MIG. Structure is [documented below](#nested_on_repair). +* `on_repair` - (Optional), Configuration for VM repairs in the MIG. Structure is [documented below](#nested_on_repair). - - - The `on_repair` block supports: From a6c2ea4476a7c8979b36348e90fac6f53792c2f8 Mon Sep 17 00:00:00 2001 From: "Stephen Lewis (Burrows)" Date: Tue, 30 Jun 2026 11:55:49 -0700 Subject: [PATCH 13/78] Fixed boundary error in SamplePrimaryResourceId (#18112) --- mmv1/api/resource.go | 3 + mmv1/api/resource_test.go | 125 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+) diff --git a/mmv1/api/resource.go b/mmv1/api/resource.go index f8f191eb9c89..201cda61f74d 100644 --- a/mmv1/api/resource.go +++ b/mmv1/api/resource.go @@ -1841,6 +1841,9 @@ func (r Resource) SamplePrimaryResourceId() string { return (r.ProductMetadata.VersionObjOrClosest(r.TargetVersionName).CompareTo(r.ProductMetadata.VersionObjOrClosest(s.MinVersion)) < 0) }) } + if len(samples) == 0 { + return "" + } return samples[0].PrimaryResourceId } diff --git a/mmv1/api/resource_test.go b/mmv1/api/resource_test.go index cb558ad2e3b5..26e35132e687 100644 --- a/mmv1/api/resource_test.go +++ b/mmv1/api/resource_test.go @@ -990,3 +990,128 @@ func TestIdentityPropertiesFlattenObject(t *testing.T) { t.Errorf("expected IdentityProperties to include flattened identifier \"datasetId\", got %v", got) } } + +func TestSamplePrimaryResourceId(t *testing.T) { + t.Parallel() + + p := &api.Product{ + Name: "test", + Versions: []*product.Version{ + { + Name: "ga", + BaseUrl: "ga_url", + }, + { + Name: "beta", + BaseUrl: "beta_url", + }, + }, + } + + cases := []struct { + description string + resource api.Resource + expected string + }{ + { + description: "empty samples returns empty string", + resource: api.Resource{ + Samples: []*resource.Sample{}, + ProductMetadata: p, + TargetVersionName: "ga", + }, + expected: "", + }, + { + description: "samples with higher min_version returns empty string", + resource: api.Resource{ + Samples: []*resource.Sample{ + { + PrimaryResourceId: "beta-res", + MinVersion: "beta", + }, + }, + ProductMetadata: p, + TargetVersionName: "ga", + }, + expected: "", + }, + { + description: "valid sample returns primary resource id", + resource: api.Resource{ + Samples: []*resource.Sample{ + { + PrimaryResourceId: "ga-res", + }, + }, + ProductMetadata: p, + TargetVersionName: "ga", + }, + expected: "ga-res", + }, + { + description: "only the first sample should be used", + resource: api.Resource{ + Samples: []*resource.Sample{ + { + PrimaryResourceId: "first-res", + MinVersion: "ga", + }, + { + PrimaryResourceId: "second-res", + MinVersion: "ga", + }, + }, + ProductMetadata: p, + TargetVersionName: "ga", + }, + expected: "first-res", + }, + { + description: "excludetest should be honored using first non-excluded sample", + resource: api.Resource{ + Samples: []*resource.Sample{ + { + PrimaryResourceId: "excluded-res", + ExcludeTest: true, + }, + { + PrimaryResourceId: "included-res", + ExcludeTest: false, + }, + }, + ProductMetadata: p, + TargetVersionName: "ga", + }, + expected: "included-res", + }, + { + description: "fallback to first matching excluded sample when all are excluded", + resource: api.Resource{ + Samples: []*resource.Sample{ + { + PrimaryResourceId: "excluded-first", + ExcludeTest: true, + }, + { + PrimaryResourceId: "excluded-second", + ExcludeTest: true, + }, + }, + ProductMetadata: p, + TargetVersionName: "ga", + }, + expected: "excluded-first", + }, + } + + for _, tc := range cases { + tc := tc + t.Run(tc.description, func(t *testing.T) { + t.Parallel() + if got := tc.resource.SamplePrimaryResourceId(); got != tc.expected { + t.Errorf("SamplePrimaryResourceId() = %q, want %q", got, tc.expected) + } + }) + } +} From 7efdd5c39b2996203dde4b2d3cc601d80a4b6910 Mon Sep 17 00:00:00 2001 From: swethamagesh14 Date: Wed, 1 Jul 2026 01:27:28 +0530 Subject: [PATCH 14/78] Fix ogg version field in ggdeployment full test (#18123) --- .../oracledatabase_goldengate_deployment_full.tf.tmpl | 1 - 1 file changed, 1 deletion(-) diff --git a/mmv1/templates/terraform/samples/services/oracledatabase/oracledatabase_goldengate_deployment_full.tf.tmpl b/mmv1/templates/terraform/samples/services/oracledatabase/oracledatabase_goldengate_deployment_full.tf.tmpl index c4854740b16f..bd2ef8ce1f0b 100644 --- a/mmv1/templates/terraform/samples/services/oracledatabase/oracledatabase_goldengate_deployment_full.tf.tmpl +++ b/mmv1/templates/terraform/samples/services/oracledatabase/oracledatabase_goldengate_deployment_full.tf.tmpl @@ -22,7 +22,6 @@ resource "google_oracle_database_goldengate_deployment" "{{$.PrimaryResourceId}} admin_username = "admin" admin_password = "123Abpassword!" deployment = "deployment" - ogg_version = "oggoracle:23.26.2.0.0_260417.1915_14223" } maintenance_window { From 8e9b3cbb30e6903fe32142f41bf4fb1389004f9a Mon Sep 17 00:00:00 2001 From: virgulati2003 <108871727+virgulati2003@users.noreply.github.com> Date: Wed, 1 Jul 2026 02:18:52 +0530 Subject: [PATCH 15/78] Add exadataInfrastructure field and sample to ExascaleDbStorageVault (#18136) --- .../ExascaleDbStorageVault.yaml | 29 ++++++++++++++ ...t_dedicated_exadata_infrastructure.tf.tmpl | 40 +++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 mmv1/templates/terraform/samples/services/oracledatabase/oracledatabase_exascale_db_storage_vault_dedicated_exadata_infrastructure.tf.tmpl diff --git a/mmv1/products/oracledatabase/ExascaleDbStorageVault.yaml b/mmv1/products/oracledatabase/ExascaleDbStorageVault.yaml index c3c064e18bd4..b619e20e83e9 100644 --- a/mmv1/products/oracledatabase/ExascaleDbStorageVault.yaml +++ b/mmv1/products/oracledatabase/ExascaleDbStorageVault.yaml @@ -21,6 +21,10 @@ create_url: projects/{{project}}/locations/{{location}}/exascaleDbStorageVaults? id_format: projects/{{project}}/locations/{{location}}/exascaleDbStorageVaults/{{exascale_db_storage_vault_id}} import_format: - projects/{{project}}/locations/{{location}}/exascaleDbStorageVaults/{{exascale_db_storage_vault_id}} +timeouts: + insert_minutes: 120 + update_minutes: 60 + delete_minutes: 60 async: type: OpAsync operation: @@ -77,6 +81,22 @@ samples: # As a result these resources are not sweepable # See: https://github.com/hashicorp/terraform-provider-google/issues/20599 exascale_db_storage_vault_id: fmt.Sprintf("ofake-tf-test-storage-vault-full-%s", acctest.RandString(t, 10)) + - name: oracledatabase_exascale_db_storage_vault_dedicated_exadata_infrastructure + primary_resource_id: my_storage_vault + steps: + - name: oracledatabase_exascale_db_storage_vault_dedicated_exadata_infrastructure + resource_id_vars: + project: my-project + cloud_exadata_infrastructure_id: my-infra + exascale_db_storage_vault_id: my-instance + deletion_protection: true + ignore_read_extra: + - deletion_protection + test_vars_overrides: + deletion_protection: "false" + project: '"oci-terraform-testing-prod"' + cloud_exadata_infrastructure_id: 'fmt.Sprintf("ofake-tf-configured-exadata-%s", acctest.RandString(t, 10))' + exascale_db_storage_vault_id: 'fmt.Sprintf("ofake-tf-test-vault-on-exadata-%s", acctest.RandString(t, 10))' virtual_fields: - name: deletion_protection type: Boolean @@ -134,6 +154,15 @@ properties: Format: projects/{project}/locations/{location}/exascaleDbStorageVaults/{exascale_db_storage_vault} output: true + - name: exadataInfrastructure + type: ResourceRef + description: | + The Exadata Infrastructure resource on which ExascaleDbStorageVault resource is created. + In the format: projects/{project}/locations/{region}/cloudExadataInfrastructures/{cloud_extradata_infrastructure} + resource: CloudExadataInfrastructure + imports: name + immutable: true + diff_suppress_func: tpgresource.CompareSelfLinkOrResourceName - name: properties type: NestedObject description: |- diff --git a/mmv1/templates/terraform/samples/services/oracledatabase/oracledatabase_exascale_db_storage_vault_dedicated_exadata_infrastructure.tf.tmpl b/mmv1/templates/terraform/samples/services/oracledatabase/oracledatabase_exascale_db_storage_vault_dedicated_exadata_infrastructure.tf.tmpl new file mode 100644 index 000000000000..fb57e69d465b --- /dev/null +++ b/mmv1/templates/terraform/samples/services/oracledatabase/oracledatabase_exascale_db_storage_vault_dedicated_exadata_infrastructure.tf.tmpl @@ -0,0 +1,40 @@ +resource "google_oracle_database_cloud_exadata_infrastructure" "infra" { + cloud_exadata_infrastructure_id = "{{index $.ResourceIdVars "cloud_exadata_infrastructure_id"}}" + display_name = "{{index $.ResourceIdVars "cloud_exadata_infrastructure_id"}} displayname" + location = "us-east4" + project = "{{index $.ResourceIdVars "project"}}" + + properties { + shape = "Exadata.X9M" + compute_count = "2" + storage_count = "3" + } + + deletion_protection = "{{index $.ResourceIdVars "deletion_protection"}}" +} + +resource "google_oracle_database_cloud_exadata_infrastructure_exascale_config" "exascale_config" { + cloud_exadata_infrastructure = google_oracle_database_cloud_exadata_infrastructure.infra.cloud_exadata_infrastructure_id + location = "us-east4" + project = "{{index $.ResourceIdVars "project"}}" + total_storage_size_gb = 10240 +} + +resource "google_oracle_database_exascale_db_storage_vault" "{{$.PrimaryResourceId}}" { + exascale_db_storage_vault_id = "{{index $.ResourceIdVars "exascale_db_storage_vault_id"}}" + display_name = "{{index $.ResourceIdVars "exascale_db_storage_vault_id"}} displayname" + location = "us-east4" + project = "{{index $.ResourceIdVars "project"}}" + + exadata_infrastructure = google_oracle_database_cloud_exadata_infrastructure.infra.name + + depends_on = [google_oracle_database_cloud_exadata_infrastructure_exascale_config.exascale_config] + + properties { + exascale_db_storage_details { + total_size_gbs = 2048 + } + } + + deletion_protection = "{{index $.ResourceIdVars "deletion_protection"}}" +} From 2c4bbb13c151a2bfbce4a4dd5fb715fff013d828 Mon Sep 17 00:00:00 2001 From: Vladimir Lazarenko Date: Tue, 30 Jun 2026 23:08:38 +0200 Subject: [PATCH 16/78] feat(storagetransfer): add private_network_service to azure_blob_storage_data_source (#17748) --- .../resource_storage_transfer_job.go | 7 + .../resource_storage_transfer_job_meta.yaml | 1 + .../resource_storage_transfer_job_test.go | 126 ++++++++++++++++++ .../docs/r/storage_transfer_job.html.markdown | 2 + 4 files changed, 136 insertions(+) diff --git a/mmv1/third_party/terraform/services/storagetransfer/resource_storage_transfer_job.go b/mmv1/third_party/terraform/services/storagetransfer/resource_storage_transfer_job.go index b5e0ad7e3a42..21ef2d20b629 100644 --- a/mmv1/third_party/terraform/services/storagetransfer/resource_storage_transfer_job.go +++ b/mmv1/third_party/terraform/services/storagetransfer/resource_storage_transfer_job.go @@ -917,6 +917,11 @@ func azureBlobStorageDataSchema() *schema.Resource { }, Description: ` Workload Identity Details used to authenticate API requests to Azure.`, }, + "private_network_service": { + Type: schema.TypeString, + Optional: true, + Description: `Service Directory Service used as the endpoint for transfers from a customer-managed VPC. Format: projects/{projectId}/locations/{location}/namespaces/{namespace}/services/{service}`, + }, }, } } @@ -1782,6 +1787,7 @@ func expandAzureBlobStorageData(azureBlobStorageDatas []interface{}) *storagetra AzureCredentials: expandAzureCredentials(azureBlobStorageData["azure_credentials"].([]interface{})), CredentialsSecret: azureBlobStorageData["credentials_secret"].(string), FederatedIdentityConfig: expandAzureFederatedIdentifyConfig(azureBlobStorageData["federated_identity_config"].([]interface{})), + PrivateNetworkService: azureBlobStorageData["private_network_service"].(string), } } @@ -1793,6 +1799,7 @@ func flattenAzureBlobStorageData(azureBlobStorageData *storagetransfer.AzureBlob "azure_credentials": flattenAzureCredentials(d), "federated_identity_config": flattenAzureFederatedIdentifyConfig(d), "credentials_secret": azureBlobStorageData.CredentialsSecret, + "private_network_service": azureBlobStorageData.PrivateNetworkService, } return []map[string]interface{}{data} diff --git a/mmv1/third_party/terraform/services/storagetransfer/resource_storage_transfer_job_meta.yaml b/mmv1/third_party/terraform/services/storagetransfer/resource_storage_transfer_job_meta.yaml index 9bf9ba4ae234..ac10b9a1d31f 100644 --- a/mmv1/third_party/terraform/services/storagetransfer/resource_storage_transfer_job_meta.yaml +++ b/mmv1/third_party/terraform/services/storagetransfer/resource_storage_transfer_job_meta.yaml @@ -70,6 +70,7 @@ fields: - api_field: 'transferSpec.azureBlobStorageDataSource.federatedIdentityConfig.clientId' - api_field: 'transferSpec.azureBlobStorageDataSource.federatedIdentityConfig.tenantId' - api_field: 'transferSpec.azureBlobStorageDataSource.path' + - api_field: 'transferSpec.azureBlobStorageDataSource.privateNetworkService' - api_field: 'transferSpec.azureBlobStorageDataSource.storageAccount' - api_field: 'transferSpec.awsS3CompatibleDataSource.bucketName' - api_field: 'transferSpec.awsS3CompatibleDataSource.path' diff --git a/mmv1/third_party/terraform/services/storagetransfer/resource_storage_transfer_job_test.go b/mmv1/third_party/terraform/services/storagetransfer/resource_storage_transfer_job_test.go index 6294e75ea556..22d91d18da56 100644 --- a/mmv1/third_party/terraform/services/storagetransfer/resource_storage_transfer_job_test.go +++ b/mmv1/third_party/terraform/services/storagetransfer/resource_storage_transfer_job_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" "github.com/hashicorp/terraform-plugin-testing/terraform" "github.com/hashicorp/terraform-provider-google/google/acctest" "github.com/hashicorp/terraform-provider-google/google/envvar" @@ -681,6 +682,52 @@ func TestAccStorageTransferJob_awsS3CompatibleDataSource(t *testing.T) { }) } +func TestAccStorageTransferJob_azureBlobStorageDataSourcePrivateNetwork(t *testing.T) { + t.Skip("only works locally: requires a private cross-cloud interconnect between Azure and GCP that is not available in the test environment") + t.Parallel() + + project := envvar.GetTestProjectFromEnv() + testDataSinkName := acctest.RandString(t, 10) + suffix := acctest.RandString(t, 10) + namespaceId := "tf-test-sts-azure-" + suffix + serviceIdA := "tf-test-sts-azure-svc-a-" + suffix + serviceIdB := "tf-test-sts-azure-svc-b-" + suffix + location := "us-central1" + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccStorageTransferJobDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccStorageTransferJob_azureBlobStorageDataSourcePrivateNetwork(project, testDataSinkName, namespaceId, serviceIdA, serviceIdB, location, "a"), + }, + { + ResourceName: "google_storage_transfer_job.transfer_job", + ImportState: true, + ImportStateVerify: true, + // azure_credentials is input-only on the API and not returned on Read. + ImportStateVerifyIgnore: []string{"transfer_spec.0.azure_blob_storage_data_source.0.azure_credentials"}, + }, + { + Config: testAccStorageTransferJob_azureBlobStorageDataSourcePrivateNetwork(project, testDataSinkName, namespaceId, serviceIdA, serviceIdB, location, "b"), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + // private_network_service is mutable; switching it must update in-place, not recreate. + plancheck.ExpectResourceAction("google_storage_transfer_job.transfer_job", plancheck.ResourceActionUpdate), + }, + }, + }, + { + ResourceName: "google_storage_transfer_job.transfer_job", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"transfer_spec.0.azure_blob_storage_data_source.0.azure_credentials"}, + }, + }, + }) +} + func TestAccStorageTransferJob_transferManifest(t *testing.T) { t.Parallel() @@ -3071,6 +3118,85 @@ resource "google_storage_transfer_job" "transfer_job" { `, project, dataSourceBucketName, dataSinkBucketName, transferJobDescription, manifestObjectName) } +func testAccStorageTransferJob_azureBlobStorageDataSourcePrivateNetwork(project, dataSinkBucketName, namespaceId, serviceIdA, serviceIdB, location, which string) string { + pnsRef := "google_service_directory_service.sts_azure_a.id" + if which == "b" { + pnsRef = "google_service_directory_service.sts_azure_b.id" + } + return fmt.Sprintf(` +data "google_storage_transfer_project_service_account" "default" { + project = "%[1]s" +} + +resource "google_storage_bucket" "data_sink" { + name = "%[2]s" + project = "%[1]s" + location = "US" + force_destroy = true +} + +resource "google_storage_bucket_iam_member" "data_sink" { + bucket = google_storage_bucket.data_sink.name + role = "roles/storage.admin" + member = "serviceAccount:${data.google_storage_transfer_project_service_account.default.email}" +} + +resource "google_service_directory_namespace" "sts_azure" { + project = "%[1]s" + namespace_id = "%[3]s" + location = "%[6]s" +} + +resource "google_service_directory_service" "sts_azure_a" { + service_id = "%[4]s" + namespace = google_service_directory_namespace.sts_azure.id +} + +resource "google_service_directory_service" "sts_azure_b" { + service_id = "%[5]s" + namespace = google_service_directory_namespace.sts_azure.id +} + +resource "google_storage_transfer_job" "transfer_job" { + description = "Azure source with private network service" + project = "%[1]s" + + transfer_spec { + azure_blob_storage_data_source { + storage_account = "examplestorageaccount" + container = "example-container" + path = "foo/bar/" + azure_credentials { + sas_token = "fakesastoken" + } + private_network_service = %[7]s + } + gcs_data_sink { + bucket_name = google_storage_bucket.data_sink.name + path = "foo/bar/" + } + } + + schedule { + schedule_start_date { + year = 2024 + month = 1 + day = 1 + } + schedule_end_date { + year = 2024 + month = 1 + day = 1 + } + } + + depends_on = [ + google_storage_bucket_iam_member.data_sink, + ] +} +`, project, dataSinkBucketName, namespaceId, serviceIdA, serviceIdB, location, pnsRef) +} + func testAccStorageTransferJob_withoutTransferManifest(project, dataSourceBucketName, dataSinkBucketName, transferJobDescription string) string { return fmt.Sprintf(` data "google_storage_transfer_project_service_account" "default" { diff --git a/mmv1/third_party/terraform/website/docs/r/storage_transfer_job.html.markdown b/mmv1/third_party/terraform/website/docs/r/storage_transfer_job.html.markdown index a004ac000ab8..45b8df4fb385 100644 --- a/mmv1/third_party/terraform/website/docs/r/storage_transfer_job.html.markdown +++ b/mmv1/third_party/terraform/website/docs/r/storage_transfer_job.html.markdown @@ -333,6 +333,8 @@ The `aws_access_key` block supports: * `federated_identity_config` - (Optional) Federated identity config of a user registered Azure application. Structure [documented below](#nested_federated_identity_config). +* `private_network_service` - (Optional) Service Directory Service to be used as the endpoint for transfers from a customer-managed VPC. Format: `projects/{projectId}/locations/{location}/namespaces/{namespace}/services/{service}`. + The `azure_credentials` block supports: * `sas_token` - (Required) Azure shared access signature. See [Grant limited access to Azure Storage resources using shared access signatures (SAS)](https://docs.microsoft.com/en-us/azure/storage/common/storage-sas-overview). From d484bf7fabe33682e625549eb9f6f475e78d49f8 Mon Sep 17 00:00:00 2001 From: Afonso Menegola Date: Tue, 30 Jun 2026 18:10:57 -0300 Subject: [PATCH 17/78] =?UTF-8?q?feat(vertexai):=20add=20customizationConf?= =?UTF-8?q?igs=20to=20google=5Fvertex=5Fai=5Freasonin=E2=80=A6=20(#18120)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Afonso Menegola --- mmv1/products/vertexai/ReasoningEngine.yaml | 94 +++++++++++++++++++ ...x_ai_reasoning_engine_context_spec.tf.tmpl | 82 ++++++++++++++++ 2 files changed, 176 insertions(+) diff --git a/mmv1/products/vertexai/ReasoningEngine.yaml b/mmv1/products/vertexai/ReasoningEngine.yaml index ab33a657360a..4c7d39ce5949 100644 --- a/mmv1/products/vertexai/ReasoningEngine.yaml +++ b/mmv1/products/vertexai/ReasoningEngine.yaml @@ -629,3 +629,97 @@ properties: type: Boolean description: |- If true, no memory revisions will be created for any requests to the Memory Bank. + - name: 'customizationConfigs' + type: Array + description: |- + Optional. Customization configs for how Agent Engine sub-resources manage context at different scope levels. + item_type: + type: NestedObject + properties: + - name: 'scopeKeys' + type: Array + description: |- + Optional. List of scope keys that this customization config applies to. + item_type: + type: String + - name: 'memoryTopics' + type: Array + description: |- + Optional. List of topics that the memory should be associated with. + item_type: + type: NestedObject + properties: + - name: 'managedMemoryTopic' + type: NestedObject + # Note: exactly_one_of within array elements is not enforced client-side by MM; relies on backend API server validation. + exactly_one_of: + - managedMemoryTopic + - customMemoryTopic + description: |- + Optional. Managed memory topic. + properties: + - name: 'managedTopicEnum' + type: String + description: |- + Managed topic enum (e.g. USER_PREFERENCES, EXPLICIT_INSTRUCTIONS). + - name: 'customMemoryTopic' + type: NestedObject + description: |- + Optional. Custom memory topic. + properties: + - name: 'label' + type: String + description: |- + Label of custom memory topic. + - name: 'description' + type: String + description: |- + Description of custom memory topic. + - name: 'consolidationConfig' + type: NestedObject + description: |- + Optional. Configuration for how many memory revisions Memory Bank considers when consolidating each memory candidate. + properties: + - name: 'revisionsPerCandidateCount' + type: Integer + description: |- + Number of revisions to consider per candidate count. + - name: 'enableThirdPersonMemories' + type: Boolean + description: |- + Optional. Generate memories in the third person if set to true. + - name: 'structuredMemoryConfigs' + type: Array + description: |- + Optional. Structured memory configurations for Agent Engine sub-resources. + item_type: + type: NestedObject + properties: + - name: 'scopeKeys' + type: Array + description: |- + Optional. List of scope keys that this structured memory config applies to. + item_type: + type: String + - name: 'schemaConfigs' + type: Array + description: |- + Optional. List of schema configs that this structured memory config applies to. + item_type: + type: NestedObject + properties: + - name: 'id' + type: String + description: |- + Required. Unique ID identifying the memory schema. + required: true + - name: 'memorySchema' + type: String + api_name: 'schema' + description: |- + Optional. The memory schema defined as an OpenAPI Schema Object JSON string. + state_func: 'func(v interface{}) string { s, _ := structure.NormalizeJsonString(v); return s }' + custom_flatten: 'templates/terraform/custom_flatten/json_schema.tmpl' + custom_expand: 'templates/terraform/custom_expand/json_value.tmpl' + validation: + function: 'validation.StringIsJSON' diff --git a/mmv1/templates/terraform/samples/services/vertexai/vertex_ai_reasoning_engine_context_spec.tf.tmpl b/mmv1/templates/terraform/samples/services/vertexai/vertex_ai_reasoning_engine_context_spec.tf.tmpl index f47b634a5e9d..98186fb18c8d 100644 --- a/mmv1/templates/terraform/samples/services/vertexai/vertex_ai_reasoning_engine_context_spec.tf.tmpl +++ b/mmv1/templates/terraform/samples/services/vertexai/vertex_ai_reasoning_engine_context_spec.tf.tmpl @@ -13,6 +13,88 @@ resource "google_vertex_ai_reasoning_engine" "{{$.PrimaryResourceId}}" { embedding_model = "projects/${data.google_project.project.project_id}/locations/us-central1/publishers/google/models/text-embedding-005" } disable_memory_revisions = false + + # 1. User-level Customization Config (Lifelong baseline) + customization_configs { + scope_keys = ["user_id"] + enable_third_person_memories = true + consolidation_config { + revisions_per_candidate_count = 1 + } + memory_topics { + managed_memory_topic { + managed_topic_enum = "USER_PREFERENCES" + } + } + } + + # 2. Session-level Customization Config (Transient scratchpad) + customization_configs { + scope_keys = ["user_id", "session_id"] + enable_third_person_memories = true + memory_topics { + custom_memory_topic { + label = "session_scratchpad" + description = "Active consideration details, recent queries, and temporary workflow state." + } + } + } + + # 1. Standard User Profile Schema (from official documentation) + structured_memory_configs { + scope_keys = ["user_id"] + schema_configs { + id = "user-profile" + memory_schema = jsonencode({ + type = "OBJECT" + properties = { + name = { + type = "STRING" + description = "Name of the user." + } + technical_stack = { + type = "STRING" + description = "Comma-separated list tools or languages used by the user." + } + primary_goal = { + type = "STRING" + description = "The main objective the user is pursuing." + } + expertise_level = { + type = "STRING" + description = "Current skill level (e.g., Junior, Senior)." + } + job_status = { + type = "STRING" + description = "The job status of the individual" + enum = ["unemployed", "part_time", "full_time", "student"] + } + } + }) + } + } + + # 2. Conversation Summary Schema (Demonstrating session scope) + structured_memory_configs { + scope_keys = ["user_id", "session_id"] + schema_configs { + id = "conversation-summary" + memory_schema = jsonencode({ + type = "OBJECT" + properties = { + main_topic = { + type = "STRING" + description = "The primary topic of this specific chat session." + } + status = { + type = "STRING" + description = "Current resolution state of the discussion." + enum = ["open", "in_progress", "resolved"] + } + } + }) + } + } ttl_config { default_ttl = "86400s" } From bea80238c3456f2cdf4e0084bcfe5b79cdc1cff3 Mon Sep 17 00:00:00 2001 From: amukul-25 Date: Wed, 1 Jul 2026 03:59:19 +0530 Subject: [PATCH 18/78] Add new TagBindingCollection resource (#17930) Co-authored-by: Sam Levenick --- ...source_tags_tag_binding_collection.go.tmpl | 472 +++++ ...tags_tag_binding_collection_meta.yaml.tmpl | 15 + ...e_tags_tag_binding_collection_test.go.tmpl | 1820 +++++++++++++++++ ..._tag_binding_collection.html.markdown.tmpl | 127 ++ 4 files changed, 2434 insertions(+) create mode 100644 mmv1/third_party/terraform/services/tags/resource_tags_tag_binding_collection.go.tmpl create mode 100644 mmv1/third_party/terraform/services/tags/resource_tags_tag_binding_collection_meta.yaml.tmpl create mode 100644 mmv1/third_party/terraform/services/tags/resource_tags_tag_binding_collection_test.go.tmpl create mode 100644 mmv1/third_party/terraform/website/docs/r/tags_tag_binding_collection.html.markdown.tmpl diff --git a/mmv1/third_party/terraform/services/tags/resource_tags_tag_binding_collection.go.tmpl b/mmv1/third_party/terraform/services/tags/resource_tags_tag_binding_collection.go.tmpl new file mode 100644 index 000000000000..0bbba05b9091 --- /dev/null +++ b/mmv1/third_party/terraform/services/tags/resource_tags_tag_binding_collection.go.tmpl @@ -0,0 +1,472 @@ +package tags + +{{- if ne $.TargetVersionName "ga" }} + +import ( + "context" + "fmt" + "log" + "net/http" + "net/url" + "strings" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/hashicorp/terraform-provider-google/google/registry" + "github.com/hashicorp/terraform-provider-google/google/tpgresource" + transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" +) + +func ResourceTagsTagBindingCollection() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceTagsTagBindingCollectionUpsert, // Use Upsert + ReadContext: resourceTagsTagBindingCollectionRead, + UpdateContext: resourceTagsTagBindingCollectionUpsert, // Use Upsert + DeleteContext: resourceTagsTagBindingCollectionDelete, + + Importer: &schema.ResourceImporter{ + StateContext: resourceTagsTagBindingCollectionImport, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(20 * time.Minute), + Update: schema.DefaultTimeout(20 * time.Minute), + Delete: schema.DefaultTimeout(20 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "location": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Default: "global", + Description: `The location of the TagBindingCollection.`, + }, + "full_resource_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + DiffSuppressFunc: tpgresource.ProjectNumberDiffSuppress, + Description: `The full resource name of the resource to which the tags are bound. E.g. //cloudresourcemanager.googleapis.com/projects/123`, + }, + "tags": { + Type: schema.TypeMap, + Required: true, + Description: `A map of tag keys to values directly bound to this resource, specified in namespaced format. +For example: + "123/environment": "production" +Keys must be namespaced names of TagKeys, and values must be short names of TagValues. +This field is non-authoritative. Terraform will only manage the precise tags present in this map.`, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "active_tags": { + Type: schema.TypeMap, + Computed: true, + Description: `(Output) The most recent state of all direct tags on the resource, as reported by the API. +This includes the tags configured through Terraform, Google system tags, and tags attached by other clients.`, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "name": { + Type: schema.TypeString, + Computed: true, + Description: `The name of the TagBindingCollection, in the format: +locations/{location}/tagBindingCollections/{encoded_full_resource_name}`, + }, + }, + + UseJSONNumber: true, + } +} + +// Custom Read Function - Essential for non-authoritative sync +func resourceTagsTagBindingCollectionRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + config := meta.(*transport_tpg.Config) + userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) + if err != nil { + return diag.FromErr(err) + } + + location, resourceName, encodedResourceName := getTagBindingCollectionInfo(d) + currentRes, currentTags, _, err := getCurrentTagBindingCollectionState(d, config, userAgent, location, encodedResourceName) + if err != nil { + return diag.FromErr(transport_tpg.HandleNotFoundError(err, d, fmt.Sprintf("TagBindingCollection %q", d.Id()))) + } + + if err := d.Set("location", location); err != nil { + return diag.Errorf("error setting location: %s", err) + } + if err := d.Set("full_resource_name", resourceName); err != nil { + return diag.Errorf("error setting full_resource_name: %s", err) + } + if err := d.Set("name", currentRes["name"]); err != nil { + return diag.Errorf("error setting name: %s", err) + } + + if err := d.Set("active_tags", currentTags); err != nil { + return diag.Errorf("error setting active_tags: %s", err) + } + + // Sync "tags" to detect drift for configured keys + if currentTags != nil { + transformedTags := make(map[string]interface{}) + if v, ok := d.GetOk("tags"); ok { + for k := range v.(map[string]interface{}) { + if val, exists := currentTags[k]; exists { + transformedTags[k] = val + } + } + } + if err := d.Set("tags", transformedTags); err != nil { + return diag.Errorf("error setting tags: %s", err) + } + } + + return nil +} + +// Custom Upsert Function for Create and Update +func resourceTagsTagBindingCollectionUpsert(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + config := meta.(*transport_tpg.Config) + userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) + if err != nil { + return diag.FromErr(err) + } + + location, resourceName, encodedResourceName := getTagBindingCollectionInfo(d) + + configuredTags, diags := getStringMapFromSchema(d, "tags") + if diags != nil { + return diags + } + + timeout := d.Timeout(schema.TimeoutUpdate) + opDesc := "Updating TagBindingCollection" + if d.IsNewResource() { + timeout = d.Timeout(schema.TimeoutCreate) + opDesc = "Creating TagBindingCollection" + } + + backoff := time.Second + for { + // Step 1: Read current state + _, currentTags, currentEtag, err := getCurrentTagBindingCollectionState(d, config, userAgent, location, encodedResourceName) + if err != nil { + return diag.Errorf("error reading current state for upsert: %s", err) + } + + // Step 2: Prepare PATCH object + obj := make(map[string]interface{}) + obj["full_resource_name"] = resourceName + if currentEtag != "" { + obj["etag"] = currentEtag + } + + log.Printf("[DEBUG] Current Tags %#v", currentTags) + log.Printf("[DEBUG] Configured Tags %#v", configuredTags) + + mergedTags := make(map[string]interface{}) + for k, v := range currentTags { + mergedTags[k] = v + } + for k, v := range configuredTags { + // This will also overwrite the a key with value as configured in terraform, nullifying any updates to the same binding outside of terraform + mergedTags[k] = v + } + + // Remove tags that were in the old state but not in the new config + if !d.IsNewResource() { + oldTagsIFace, _ := d.GetChange("tags") + oldTags, diags := convertInterfaceToStringMap(oldTagsIFace) + if diags != nil { + return diags + } + log.Printf("[DEBUG] Old Tags %#v", oldTags) + for k := range oldTags { + if _, existsInNewConfig := configuredTags[k]; !existsInNewConfig { + delete(mergedTags, k) + } + } + } + log.Printf("[DEBUG] Merged Tags %#v", mergedTags) + obj["tags"] = mergedTags + + // Step 3: Write + err = patchTagBindingCollection(config, userAgent, d, location, encodedResourceName, obj, timeout, opDesc) + if err != nil { + if transport_tpg.IsGoogleApiErrorWithCode(err, 412) || transport_tpg.IsGoogleApiErrorWithCode(err, 409) { + log.Printf("[DEBUG] Concurrent ETag collision detected during %s, restarting read-modify-write cycle after %s", opDesc, backoff) + time.Sleep(backoff) + backoff = backoff * 2 + if backoff > 30*time.Second { + return diag.Errorf("Error %s: Too many concurrent ETag modifications on parent %q. Final error: %s", opDesc, resourceName, err) + } + continue + } + return diag.Errorf("Error executing PATCH request for %s: %s", opDesc, err) + } + break + } + + if d.IsNewResource() { + d.SetId(fmt.Sprintf("locations/%s/tagBindingCollections/%s", location, encodedResourceName)) + } + + return resourceTagsTagBindingCollectionRead(ctx, d, meta) +} + +func resourceTagsTagBindingCollectionDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + config := meta.(*transport_tpg.Config) + userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) + if err != nil { + return diag.FromErr(err) + } + + location, resourceName, encodedResourceName := getTagBindingCollectionInfo(d) + + // Step 1: Get Terraform-managed old tags from state + oldTagsIFace, _ := d.GetChange("tags") + oldTags, diags := convertInterfaceToStringMap(oldTagsIFace) + if diags != nil { + return diags + } + + timeout := d.Timeout(schema.TimeoutDelete) + opDesc := "Deleting Terraform-managed tags" + backoff := time.Second + + for { + // Read current state + _, currentTags, currentEtag, err := getCurrentTagBindingCollectionState(d, config, userAgent, location, encodedResourceName) + if err != nil { + return diag.FromErr(transport_tpg.HandleNotFoundError(err, d, fmt.Sprintf("TagBindingCollection %q", resourceName))) + } + + // Compute the final set of tags for the PATCH request + // (Start with all current tags, then remove the ones TF managed) + finalTags := make(map[string]interface{}) + for k, v := range currentTags { + finalTags[k] = v + } + + hasTagsToDelete := false + for k := range oldTags { + if _, existsInCurrent := currentTags[k]; existsInCurrent { + delete(finalTags, k) + hasTagsToDelete = true + } + } + + if !hasTagsToDelete { + log.Printf("[DEBUG] No Terraform-managed tags to remove for %s.", resourceName) + d.SetId("") + return nil + } + + // Send PATCH request + obj := make(map[string]interface{}) + obj["full_resource_name"] = resourceName + if currentEtag != "" { + obj["etag"] = currentEtag + } + obj["tags"] = finalTags + + err = patchTagBindingCollection(config, userAgent, d, location, encodedResourceName, obj, timeout, opDesc) + if err != nil { + if transport_tpg.IsGoogleApiErrorWithCode(err, 412) || transport_tpg.IsGoogleApiErrorWithCode(err, 409) { + log.Printf("[DEBUG] Concurrent ETag collision detected during %s, restarting read-modify-write cycle after %s", opDesc, backoff) + time.Sleep(backoff) + backoff = backoff * 2 + if backoff > 30*time.Second { + return diag.Errorf("Error %s: Too many concurrent ETag modifications on parent %q. Final error: %s", opDesc, resourceName, err) + } + continue + } + return diag.Errorf("Error executing PATCH request for %s: %s", opDesc, err) + } + break + } + + d.SetId("") + log.Printf("[INFO] Successfully removed Terraform-managed tags for %s", resourceName) + return nil +} + +func resourceTagsTagBindingCollectionImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + // Import ID format: locations/{location}/tagBindingCollections/{encoded_full_resource_name} + // The {encoded_full_resource_name} part is a encoded full resource name. + id := d.Id() + parts := strings.SplitN(id, "/", 4) + if len(parts) != 4 || parts[0] != "locations" || parts[2] != "tagBindingCollections" { + return nil, fmt.Errorf("invalid import ID: %s. Expected format: locations/{location}/tagBindingCollections/{encoded_full_resource_name}", id) + } + + location := parts[1] + encodedResourceName := parts[3] + + if location == "" || encodedResourceName == "" { + return nil, fmt.Errorf("invalid import ID: %s. Location and encoded_full_resource_name cannot be empty", id) + } + + resourceName, err := url.PathUnescape(encodedResourceName) + if err != nil { + return nil, fmt.Errorf("error decoding full_resource_name from import ID %q: %s", encodedResourceName, err) + } + + if err := d.Set("location", location); err != nil { + return nil, err + } + if err := d.Set("full_resource_name", resourceName); err != nil { + return nil, err + } + + // The ID is already in the desired format, so no need to d.SetId(id) again + return []*schema.ResourceData{d}, nil +} + +func getTagBindingCollectionInfo(d *schema.ResourceData) (location, resourceName, encodedResourceName string) { + location = d.Get("location").(string) + resourceName = d.Get("full_resource_name").(string) + encodedResourceName = url.PathEscape(resourceName) + return location, resourceName, encodedResourceName +} + +func buildTagBindingCollectionUrl(d *schema.ResourceData, config *transport_tpg.Config, location, encodedResourceName string) (string, error) { + basePath := "{{"{{"}}TagsLocationBasePath{{"}}"}}" + if location == "global" { + basePath = "{{"{{"}}TagsBasePath{{"}}"}}" + } + return tpgresource.ReplaceVars(d, config, fmt.Sprintf("%slocations/%s/tagBindingCollections/%s", basePath, location, encodedResourceName)) +} + +func flattenTagsTagBindingCollectionTags(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + return v +} + +// Helper to fetch the current state of the TagBindingCollection from GCP +func getCurrentTagBindingCollectionState(d *schema.ResourceData, config *transport_tpg.Config, userAgent, location, encodedResourceName string) (currentRes map[string]interface{}, currentTags map[string]string, currentEtag string, err error) { + readUrl, err := buildTagBindingCollectionUrl(d, config, location, encodedResourceName) + if err != nil { + return nil, nil, "", err + } + headers := make(http.Header) + + log.Printf("[DEBUG] Reading current TagBindingCollection state with URL %s", readUrl) + currentRes, err = transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "GET", + RawURL: readUrl, + UserAgent: userAgent, + Headers: headers, + }) + + if err != nil { + return nil, nil, "", err + } + + currentEtag, _ = currentRes["etag"].(string) + + currentTags = make(map[string]string) + currentApiTags := currentRes["tags"] + if currentApiTags != nil { + currentTagsIFace, ok := currentApiTags.(map[string]interface{}) + if !ok { + return nil, nil, currentEtag, fmt.Errorf("API returned 'tags' in an unexpected format: %T", currentApiTags) + } + currentTags, err = convertToStringMap(currentTagsIFace) + if err != nil { + return nil, nil, currentEtag, fmt.Errorf("error converting current API tags: %s", err) + } + } + + return currentRes, currentTags, currentEtag, nil +} + +// Helper to send a PATCH request for TagBindingCollection and wait for the operation +func patchTagBindingCollection(config *transport_tpg.Config, userAgent string, d *schema.ResourceData, location, encodedResourceName string, patchObj map[string]interface{}, timeout time.Duration, operationDesc string) error { + patchUrl, err := buildTagBindingCollectionUrl(d, config, location, encodedResourceName) + if err != nil { + return err + } + patchUrl, err = transport_tpg.AddQueryParams(patchUrl, map[string]string{"updateMask": "*"}) + if err != nil { + return err + } + + billingProject := "" + if bp, err := tpgresource.GetBillingProject(d, config); err == nil { + billingProject = bp + } + + headers := make(http.Header) + + log.Printf("[DEBUG] Patching TagBindingCollection with URL %s (%s): %#v", patchUrl, operationDesc, patchObj) + initialOp, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "PATCH", + Project: billingProject, + RawURL: patchUrl, + UserAgent: userAgent, + Body: patchObj, + Headers: headers, + }) + if err != nil { + return fmt.Errorf("error starting PATCH request for %s: %w", operationDesc, err) + } + + err = TagsOperationWaitTime(config, initialOp, operationDesc, userAgent, timeout) + if err != nil { + return fmt.Errorf("error waiting for PATCH operation %s: %w", operationDesc, err) + } + + log.Printf("[INFO] Successfully completed PATCH request for %s", operationDesc) + return nil +} + +// Helper to get a map[string]string from schema.ResourceData +func getStringMapFromSchema(d *schema.ResourceData, key string) (map[string]string, diag.Diagnostics) { + val := d.Get(key) + return convertInterfaceToStringMap(val) +} + +// Helper to safely convert interface{} to map[string]string +func convertInterfaceToStringMap(iface interface{}) (map[string]string, diag.Diagnostics) { + if iface == nil { + return make(map[string]string), nil + } + mapIFace, ok := iface.(map[string]interface{}) + if !ok { + return nil, diag.Errorf("input is not a map[string]interface{}: %T", iface) + } + res, err := convertToStringMap(mapIFace) + if err != nil { + return nil, diag.FromErr(err) + } + return res, nil +} + +// convertToStringMap safely converts a map[string]interface{} to map[string]string. +// It returns an error if any value in the map is not a string. +func convertToStringMap(in map[string]interface{}) (map[string]string, error) { + out := make(map[string]string) + for k, v := range in { + s, ok := v.(string) + if !ok { + return nil, fmt.Errorf("value for key %q is not a string (got %T)", k, v) + } + out[k] = s + } + return out, nil +} + +func init() { + registry.Schema{ + Name: "google_tags_tag_binding_collection", + ProductName: "tags", + Type: registry.SchemaTypeResource, + Schema: ResourceTagsTagBindingCollection(), + }.Register() +} + +{{- end }} diff --git a/mmv1/third_party/terraform/services/tags/resource_tags_tag_binding_collection_meta.yaml.tmpl b/mmv1/third_party/terraform/services/tags/resource_tags_tag_binding_collection_meta.yaml.tmpl new file mode 100644 index 000000000000..91273881735c --- /dev/null +++ b/mmv1/third_party/terraform/services/tags/resource_tags_tag_binding_collection_meta.yaml.tmpl @@ -0,0 +1,15 @@ +{{ if ne $.TargetVersionName "ga" -}} +resource: 'google_tags_tag_binding_collection' +generation_type: 'handwritten' +api_service_name: 'cloudresourcemanager.googleapis.com' +api_version: 'v3' +api_resource_type_kind: 'TagBindingCollection' +fields: + - field: 'location' + - field: 'full_resource_name' + - api_field: 'name' + - field: 'active_tags' + provider_only: true + - field: 'tags' + provider_only: true +{{ end -}} diff --git a/mmv1/third_party/terraform/services/tags/resource_tags_tag_binding_collection_test.go.tmpl b/mmv1/third_party/terraform/services/tags/resource_tags_tag_binding_collection_test.go.tmpl new file mode 100644 index 000000000000..d5ba95cd9f16 --- /dev/null +++ b/mmv1/third_party/terraform/services/tags/resource_tags_tag_binding_collection_test.go.tmpl @@ -0,0 +1,1820 @@ +package tags_test + +{{- if ne $.TargetVersionName "ga" }} + +import ( + "context" + "fmt" + "net/http" + "net/url" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + + "github.com/hashicorp/terraform-provider-google/google/acctest" + "github.com/hashicorp/terraform-provider-google/google/envvar" + "github.com/hashicorp/terraform-provider-google/google/services/tags" + "github.com/hashicorp/terraform-provider-google/google/tpgresource" + transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" +) + +// Acceptance Tests +func TestAccTagsTagBindingCollection_basic(t *testing.T) { + t.Parallel() + + orgId := envvar.GetTestOrgFromEnv(t) + context := map[string]interface{}{ + "org_id": orgId, + "cc_key_short_name": "tf-test-cc-" + acctest.RandString(t, 6), + "cc_marketing_value_short_name": "tf-test-mkt-" + acctest.RandString(t, 6), + "cc_finance_value_short_name": "tf-test-fin-" + acctest.RandString(t, 6), + "env_key_short_name": "tf-test-env-" + acctest.RandString(t, 6), + "env_prod_value_short_name": "tf-test-prod-" + acctest.RandString(t, 6), + "new_key_short_name": "tf-test-new-" + acctest.RandString(t, 6), + "new_value_short_name": "tf-test-newval-" + acctest.RandString(t, 6), + "project_id": "tf-test-" + acctest.RandString(t, 10), + } + + // Expected namespaced keys + envKeyNamespaced := fmt.Sprintf("%s/%s", orgId, context["env_key_short_name"]) + ccKeyNamespaced := fmt.Sprintf("%s/%s", orgId, context["cc_key_short_name"]) + newKeyNamespaced := fmt.Sprintf("%s/%s", orgId, context["new_key_short_name"]) + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderBetaFactories(t), + Steps: []resource.TestStep{ + // Step 1: Create with initial set of tags + { + Config: testAccTagsTagBindingCollection_basic(context), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.basic", "tags.%", "2"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.basic", "active_tags.%", "2"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.basic", fmt.Sprintf("tags.%s", envKeyNamespaced), context["env_prod_value_short_name"].(string)), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.basic", fmt.Sprintf("tags.%s", ccKeyNamespaced), context["cc_marketing_value_short_name"].(string)), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.basic", fmt.Sprintf("active_tags.%s", envKeyNamespaced), context["env_prod_value_short_name"].(string)), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.basic", fmt.Sprintf("active_tags.%s", ccKeyNamespaced), context["cc_marketing_value_short_name"].(string)), + resource.TestMatchResourceAttr("google_tags_tag_binding_collection.basic", "id", regexp.MustCompile(fmt.Sprintf("locations/global/tagBindingCollections/.+"))), + resource.TestMatchResourceAttr("google_tags_tag_binding_collection.basic", "name", regexp.MustCompile(fmt.Sprintf("locations/global/tagBindingCollections/.+"))), + ), + }, + // Step 2: Import + { + ResourceName: "google_tags_tag_binding_collection.basic", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"tags"}, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.basic", "tags.%", "0"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.basic", "active_tags.%", "2"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.basic", "location", "global"), + ), + }, + // Step 3: Update - modify one tag, remove one tag, add one tag + { + Config: testAccTagsTagBindingCollection_update(context), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.basic", "tags.%", "2"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.basic", "active_tags.%", "2"), + // env_key was removed from config, should be gone + resource.TestCheckNoResourceAttr("google_tags_tag_binding_collection.basic", fmt.Sprintf("tags.%s", envKeyNamespaced)), + // cc_key value changed + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.basic", fmt.Sprintf("tags.%s", ccKeyNamespaced), context["cc_finance_value_short_name"].(string)), + // new_key was added + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.basic", fmt.Sprintf("tags.%s", newKeyNamespaced), context["new_value_short_name"].(string)), + // Check active_tags + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.basic", fmt.Sprintf("active_tags.%s", ccKeyNamespaced), context["cc_finance_value_short_name"].(string)), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.basic", fmt.Sprintf("active_tags.%s", newKeyNamespaced), context["new_value_short_name"].(string)), + ), + }, + // Step 4: Empty - Remove all Terraform-managed tags + { + Config: testAccTagsTagBindingCollection_empty(context), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.basic", "tags.%", "0"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.basic", "active_tags.%", "0"), + ), + }, + }, + }) +} + +func TestAccTagsTagBindingCollection_locationDefault(t *testing.T) { + t.Parallel() + + orgId := envvar.GetTestOrgFromEnv(t) + context := map[string]interface{}{ + "org_id": orgId, + "cc_key_short_name": "tf-test-cc-" + acctest.RandString(t, 6), + "cc_marketing_value_short_name": "tf-test-mkt-" + acctest.RandString(t, 6), + "env_key_short_name": "tf-test-env-" + acctest.RandString(t, 6), + "env_prod_value_short_name": "tf-test-prod-" + acctest.RandString(t, 6), + "project_id": "tf-test-" + acctest.RandString(t, 10), + } + + envKeyNamespaced := fmt.Sprintf("%s/%s", orgId, context["env_key_short_name"]) + ccKeyNamespaced := fmt.Sprintf("%s/%s", orgId, context["cc_key_short_name"]) + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderBetaFactories(t), + Steps: []resource.TestStep{ + { + Config: testAccTagsTagBindingCollection_locationDefault(context), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.location_default", "location", "global"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.location_default", "tags.%", "2"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.location_default", fmt.Sprintf("tags.%s", envKeyNamespaced), context["env_prod_value_short_name"].(string)), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.location_default", fmt.Sprintf("tags.%s", ccKeyNamespaced), context["cc_marketing_value_short_name"].(string)), + resource.TestMatchResourceAttr("google_tags_tag_binding_collection.location_default", "id", regexp.MustCompile(fmt.Sprintf("locations/global/tagBindingCollections/.+"))), + resource.TestMatchResourceAttr("google_tags_tag_binding_collection.location_default", "name", regexp.MustCompile(fmt.Sprintf("locations/global/tagBindingCollections/.+"))), + ), + }, + }, + }) +} + +func TestAccTagsTagBindingCollection_destroy(t *testing.T) { + t.Parallel() + + orgId := envvar.GetTestOrgFromEnv(t) + context := map[string]interface{}{ + "org_id": orgId, + "key_a_short": "tf-test-ka-" + acctest.RandString(t, 6), + "val_a_short": "tf-test-va-" + acctest.RandString(t, 6), + "key_b_short": "tf-test-kb-" + acctest.RandString(t, 6), + "val_b_short": "tf-test-vb-" + acctest.RandString(t, 6), + "project_id": "tf-test-" + acctest.RandString(t, 10), + } + + keyANamespaced := fmt.Sprintf("%s/%s", orgId, context["key_a_short"]) + keyBNamespaced := fmt.Sprintf("%s/%s", orgId, context["key_b_short"]) + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderBetaFactories(t), + Steps: []resource.TestStep{ + // Step 1: Create collection with TagA and a separate binding with TagB + { + Config: testAccTagsTagBindingCollection_destroyStep1(context), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.test", "tags.%", "1"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.test", "active_tags.%", "2"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.test", fmt.Sprintf("tags.%s", keyANamespaced), context["val_a_short"].(string)), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.test", fmt.Sprintf("active_tags.%s", keyANamespaced), context["val_a_short"].(string)), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.test", fmt.Sprintf("active_tags.%s", keyBNamespaced), context["val_b_short"].(string)), + resource.TestMatchResourceAttr("google_tags_tag_binding_collection.test", "id", regexp.MustCompile(fmt.Sprintf("locations/global/tagBindingCollections/.+"))), + resource.TestMatchResourceAttr("google_tags_tag_binding_collection.test", "name", regexp.MustCompile(fmt.Sprintf("locations/global/tagBindingCollections/.+"))), + ), + }, + // Step 2: Destroy the collection resource + { + Config: testAccTagsTagBindingCollection_destroyStep2(context), + Check: resource.ComposeTestCheckFunc( + // Collection is deleted from state, so we need a custom check on the API + // We check that the "other_binding" binding (TagB) still exists. + // This check is implicitly done by Terraform not erroring on the google_tags_tag_binding.other_binding resource. + + // We also need to verify TagA is deleted. This requires a custom API check + // within a CheckFunc, as the collection resource is no longer in the state. + testAccCheckTagADeleted(t, context), + ), + }, + }, + }) +} + +func TestAccTagsTagBindingCollection_outOfBandUpdate(t *testing.T) { + t.Parallel() + + orgId := envvar.GetTestOrgFromEnv(t) + context := map[string]interface{}{ + "org_id": orgId, + "project_id": envvar.GetTestProjectFromEnv(), + "instance_name": "tf-test-vm-oob-" + acctest.RandString(t, 10), + "zone": "us-central1-a", + "key_1_short": "tf-test-k1-" + acctest.RandString(t, 6), + "val_1_short": "tf-test-v1-" + acctest.RandString(t, 6), // Initial value + "val_2_short": "tf-test-v2-" + acctest.RandString(t, 6), // Value to change to externally + } + + keyNamespaced := fmt.Sprintf("%s/%s", orgId, context["key_1_short"]) + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderBetaFactories(t), + Steps: []resource.TestStep{ + // Step 1: Create the collection with initial tag value (val_1) on Compute VM + { + Config: testAccTagsTagBindingCollection_oob_config(context), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.test_oob", "tags.%", "1"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.test_oob", "active_tags.%", "1"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.test_oob", fmt.Sprintf("tags.%s", keyNamespaced), context["val_1_short"].(string)), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.test_oob", fmt.Sprintf("active_tags.%s", keyNamespaced), context["val_1_short"].(string)), + ), + }, + // Step 2: Intermediate PlanOnly step that mutates the physical server in its Check helper! + { + PlanOnly: true, + ExpectNonEmptyPlan: false, + Config: testAccTagsTagBindingCollection_oob_config(context), + Check: resource.ComposeTestCheckFunc( + testAccCheckBindingUpdatedExternally_VM(t, context, keyNamespaced, context["val_2_short"].(string)), + testAccCheckBindingValueViaAPI_VM(t, context, keyNamespaced, context["val_2_short"].(string)), + ), + }, + // Step 3: Let Terraform execute apply to flawlessly reconcile the drift back to val_1! + { + Config: testAccTagsTagBindingCollection_oob_config(context), + PlanOnly: false, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.test_oob", "tags.%", "1"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.test_oob", "active_tags.%", "1"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.test_oob", fmt.Sprintf("tags.%s", keyNamespaced), context["val_1_short"].(string)), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.test_oob", fmt.Sprintf("active_tags.%s", keyNamespaced), context["val_1_short"].(string)), + testAccCheckBindingValueViaAPI_VM(t, context, keyNamespaced, context["val_1_short"].(string)), + ), + }, + }, + }) +} + +func TestAccTagsTagBindingCollection_curatedAndDynamicMix(t *testing.T) { + t.Parallel() + + orgId := envvar.GetTestOrgFromEnv(t) + context := map[string]interface{}{ + "org_id": orgId, + "curated_key_short": "tf-test-cur-" + acctest.RandString(t, 6), + "curated_val_short": "tf-test-cval-" + acctest.RandString(t, 6), + "dynamic_key_short": "tf-test-dyn-" + acctest.RandString(t, 6), + "dynamic_bind_val": "dynamic" + acctest.RandString(t, 4), + "project_id": "tf-test-" + acctest.RandString(t, 10), + } + + curatedKeyNamespaced := fmt.Sprintf("%s/%s", orgId, context["curated_key_short"]) + dynamicKeyNamespaced := fmt.Sprintf("%s/%s", orgId, context["dynamic_key_short"]) + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderBetaFactories(t), + Steps: []resource.TestStep{ + // Step 1: Create Collection with a mix of Curated and Dynamic Tag Bindings + { + Config: testAccTagsTagBindingCollection_mixConfig(context), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.mix", "tags.%", "2"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.mix", "active_tags.%", "2"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.mix", fmt.Sprintf("tags.%s", curatedKeyNamespaced), context["curated_val_short"].(string)), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.mix", fmt.Sprintf("tags.%s", dynamicKeyNamespaced), context["dynamic_bind_val"].(string)), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.mix", fmt.Sprintf("active_tags.%s", curatedKeyNamespaced), context["curated_val_short"].(string)), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.mix", fmt.Sprintf("active_tags.%s", dynamicKeyNamespaced), context["dynamic_bind_val"].(string)), + ), + }, + }, + }) +} + +func TestAccTagsTagBindingCollection_restoration(t *testing.T) { + t.Parallel() + + orgId := envvar.GetTestOrgFromEnv(t) + context := map[string]interface{}{ + "org_id": orgId, + "project_id": envvar.GetTestProjectFromEnv(), + "instance_name": "tf-test-vm-rest-" + acctest.RandString(t, 10), + "zone": "us-central1-a", + "key_short": "tf-test-rest-" + acctest.RandString(t, 6), + "val_short": "tf-test-rval-" + acctest.RandString(t, 6), + } + + keyNamespaced := fmt.Sprintf("%s/%s", orgId, context["key_short"]) + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderBetaFactories(t), + Steps: []resource.TestStep{ + // Step 1: Create TagBindingCollection managing 1 tag on Compute VM + { + Config: testAccTagsTagBindingCollection_restorationConfig(context), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.rest", "tags.%", "1"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.rest", fmt.Sprintf("tags.%s", keyNamespaced), context["val_short"].(string)), + ), + }, + // Step 2: Intermediate PlanOnly step that deletes the physical server tag in its Check helper! + { + PlanOnly: true, + ExpectNonEmptyPlan: false, + Config: testAccTagsTagBindingCollection_restorationConfig(context), + Check: resource.ComposeTestCheckFunc( + testAccCheckBindingDeletedExternally_VM(t, context, keyNamespaced), + ), + }, + // Step 3: Let Terraform execute apply to fully and flawlessly restore the deleted tag! + { + Config: testAccTagsTagBindingCollection_restorationConfig(context), + PlanOnly: false, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.rest", "tags.%", "1"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.rest", fmt.Sprintf("tags.%s", keyNamespaced), context["val_short"].(string)), + testAccCheckBindingValueViaAPI_VM(t, context, keyNamespaced, context["val_short"].(string)), + ), + }, + }, + }) +} + +func testAccTagsTagBindingCollection_basic(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_project" "project" { + provider = google-beta + project_id = "%{project_id}" + name = "%{project_id}" + org_id = "%{org_id}" + deletion_policy = "DELETE" +} + +resource "google_tags_tag_key" "env_key" { + provider = google-beta + parent = "organizations/%{org_id}" + short_name = "%{env_key_short_name}" +} +resource "google_tags_tag_value" "env_prod_value" { + provider = google-beta + parent = google_tags_tag_key.env_key.id + short_name = "%{env_prod_value_short_name}" +} + +resource "google_tags_tag_key" "cost_center_key" { + provider = google-beta + parent = "organizations/%{org_id}" + short_name = "%{cc_key_short_name}" +} +resource "google_tags_tag_value" "cc_marketing_value" { + provider = google-beta + parent = google_tags_tag_key.cost_center_key.id + short_name = "%{cc_marketing_value_short_name}" +} + +# Resources for update test +resource "google_tags_tag_value" "cc_finance_value" { + provider = google-beta + parent = google_tags_tag_key.cost_center_key.id + short_name = "%{cc_finance_value_short_name}" +} +resource "google_tags_tag_key" "new_key" { + provider = google-beta + parent = "organizations/%{org_id}" + short_name = "%{new_key_short_name}" +} +resource "google_tags_tag_value" "new_value" { + provider = google-beta + parent = google_tags_tag_key.new_key.id + short_name = "%{new_value_short_name}" +} + +resource "google_tags_tag_binding_collection" "basic" { + provider = google-beta + full_resource_name = "//cloudresourcemanager.googleapis.com/projects/${google_project.project.number}" + location = "global" + tags = { + "${google_tags_tag_key.env_key.namespaced_name}" = google_tags_tag_value.env_prod_value.short_name + "${google_tags_tag_key.cost_center_key.namespaced_name}" = google_tags_tag_value.cc_marketing_value.short_name + } + + depends_on = [ + google_tags_tag_value.env_prod_value, + google_tags_tag_value.cc_marketing_value, + google_tags_tag_value.cc_finance_value, + google_tags_tag_value.new_value, + ] +} +`, context) +} + +func testAccTagsTagBindingCollection_update(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_project" "project" { + provider = google-beta + project_id = "%{project_id}" + name = "%{project_id}" + org_id = "%{org_id}" + deletion_policy = "DELETE" +} + +resource "google_tags_tag_key" "env_key" { + provider = google-beta + parent = "organizations/%{org_id}" + short_name = "%{env_key_short_name}" +} +resource "google_tags_tag_value" "env_prod_value" { + provider = google-beta + parent = google_tags_tag_key.env_key.id + short_name = "%{env_prod_value_short_name}" +} + +resource "google_tags_tag_key" "cost_center_key" { + provider = google-beta + parent = "organizations/%{org_id}" + short_name = "%{cc_key_short_name}" +} +resource "google_tags_tag_value" "cc_marketing_value" { + provider = google-beta + parent = google_tags_tag_key.cost_center_key.id + short_name = "%{cc_marketing_value_short_name}" +} +resource "google_tags_tag_value" "cc_finance_value" { + provider = google-beta + parent = google_tags_tag_key.cost_center_key.id + short_name = "%{cc_finance_value_short_name}" +} + +resource "google_tags_tag_key" "new_key" { + provider = google-beta + parent = "organizations/%{org_id}" + short_name = "%{new_key_short_name}" +} +resource "google_tags_tag_value" "new_value" { + provider = google-beta + parent = google_tags_tag_key.new_key.id + short_name = "%{new_value_short_name}" +} + +resource "google_tags_tag_binding_collection" "basic" { + provider = google-beta + full_resource_name = "//cloudresourcemanager.googleapis.com/projects/${google_project.project.number}" + location = "global" + tags = { + // env_key removed + "${google_tags_tag_key.cost_center_key.namespaced_name}" = google_tags_tag_value.cc_finance_value.short_name # Changed value + "${google_tags_tag_key.new_key.namespaced_name}" = google_tags_tag_value.new_value.short_name # Added tag + } + + depends_on = [ + google_tags_tag_value.env_prod_value, + google_tags_tag_value.cc_marketing_value, + google_tags_tag_value.cc_finance_value, + google_tags_tag_value.new_value, + ] +} +`, context) +} + +func testAccTagsTagBindingCollection_empty(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_project" "project" { + provider = google-beta + project_id = "%{project_id}" + name = "%{project_id}" + org_id = "%{org_id}" + deletion_policy = "DELETE" +} + +resource "google_tags_tag_key" "env_key" { + provider = google-beta + parent = "organizations/%{org_id}" + short_name = "%{env_key_short_name}" +} +resource "google_tags_tag_value" "env_prod_value" { + provider = google-beta + parent = google_tags_tag_key.env_key.id + short_name = "%{env_prod_value_short_name}" +} + +resource "google_tags_tag_key" "cost_center_key" { + provider = google-beta + parent = "organizations/%{org_id}" + short_name = "%{cc_key_short_name}" +} +resource "google_tags_tag_value" "cc_marketing_value" { + provider = google-beta + parent = google_tags_tag_key.cost_center_key.id + short_name = "%{cc_marketing_value_short_name}" +} +resource "google_tags_tag_value" "cc_finance_value" { + provider = google-beta + parent = google_tags_tag_key.cost_center_key.id + short_name = "%{cc_finance_value_short_name}" +} + +resource "google_tags_tag_key" "new_key" { + provider = google-beta + parent = "organizations/%{org_id}" + short_name = "%{new_key_short_name}" +} +resource "google_tags_tag_value" "new_value" { + provider = google-beta + parent = google_tags_tag_key.new_key.id + short_name = "%{new_value_short_name}" +} + +resource "google_tags_tag_binding_collection" "basic" { + provider = google-beta + full_resource_name = "//cloudresourcemanager.googleapis.com/projects/${google_project.project.number}" + location = "global" + tags = {} # Empty map + + depends_on = [ + google_tags_tag_value.env_prod_value, + google_tags_tag_value.cc_marketing_value, + google_tags_tag_value.cc_finance_value, + google_tags_tag_value.new_value, + ] +} +`, context) +} + +func testAccTagsTagBindingCollection_destroyStep1(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_project" "project" { + provider = google-beta + project_id = "%{project_id}" + name = "%{project_id}" + org_id = "%{org_id}" + deletion_policy = "DELETE" +} + +resource "google_tags_tag_key" "key_a" { + provider = google-beta + parent = "organizations/%{org_id}" + short_name = "%{key_a_short}" +} +resource "google_tags_tag_value" "val_a" { + provider = google-beta + parent = google_tags_tag_key.key_a.id + short_name = "%{val_a_short}" +} +resource "google_tags_tag_key" "key_b" { + provider = google-beta + parent = "organizations/%{org_id}" + short_name = "%{key_b_short}" +} +resource "google_tags_tag_value" "val_b" { + provider = google-beta + parent = google_tags_tag_key.key_b.id + short_name = "%{val_b_short}" +} + +resource "google_tags_tag_binding" "other_binding" { + provider = google-beta + parent = "//cloudresourcemanager.googleapis.com/projects/${google_project.project.number}" + tag_value = google_tags_tag_value.val_b.id + depends_on = [google_tags_tag_value.val_a, google_tags_tag_value.val_b] +} + +resource "google_tags_tag_binding_collection" "test" { + provider = google-beta + full_resource_name = "//cloudresourcemanager.googleapis.com/projects/${google_project.project.number}" + location = "global" + tags = { + "${google_tags_tag_key.key_a.namespaced_name}" = google_tags_tag_value.val_a.short_name + } + depends_on = [google_tags_tag_value.val_a, google_tags_tag_value.val_b, google_tags_tag_binding.other_binding] +} +`, context) +} + +func testAccTagsTagBindingCollection_destroyStep2(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_project" "project" { + provider = google-beta + project_id = "%{project_id}" + name = "%{project_id}" + org_id = "%{org_id}" + deletion_policy = "DELETE" +} + +resource "google_tags_tag_key" "key_a" { + provider = google-beta + parent = "organizations/%{org_id}" + short_name = "%{key_a_short}" +} +resource "google_tags_tag_value" "val_a" { + provider = google-beta + parent = google_tags_tag_key.key_a.id + short_name = "%{val_a_short}" +} +resource "google_tags_tag_key" "key_b" { + provider = google-beta + parent = "organizations/%{org_id}" + short_name = "%{key_b_short}" +} +resource "google_tags_tag_value" "val_b" { + provider = google-beta + parent = google_tags_tag_key.key_b.id + short_name = "%{val_b_short}" +} + +resource "google_tags_tag_binding" "other_binding" { + provider = google-beta + parent = "//cloudresourcemanager.googleapis.com/projects/${google_project.project.number}" + tag_value = google_tags_tag_value.val_b.id + depends_on = [google_tags_tag_value.val_a, google_tags_tag_value.val_b] +} +`, context) +} + +func testAccTagsTagBindingCollection_oob_config(context map[string]interface{}) string { + return acctest.Nprintf(` +data "google_compute_image" "my_image" { + provider = google-beta + family = "debian-11" + project = "debian-cloud" +} + +resource "google_compute_instance" "instance" { + provider = google-beta + name = "%{instance_name}" + machine_type = "e2-micro" + zone = "%{zone}" + + boot_disk { + initialize_params { + image = data.google_compute_image.my_image.self_link + } + } + + network_interface { + network = "default" + } +} + +resource "google_tags_tag_key" "key_1" { + provider = google-beta + parent = "organizations/%{org_id}" + short_name = "%{key_1_short}" +} + +resource "google_tags_tag_value" "key_1_val_1" { + provider = google-beta + parent = google_tags_tag_key.key_1.id + short_name = "%{val_1_short}" +} + +resource "google_tags_tag_value" "key_1_val_2" { + provider = google-beta + parent = google_tags_tag_key.key_1.id + short_name = "%{val_2_short}" +} + +resource "google_tags_tag_binding_collection" "test_oob" { + provider = google-beta + full_resource_name = "//compute.googleapis.com/projects/%{project_id}/zones/%{zone}/instances/${google_compute_instance.instance.instance_id}" + location = "us-central1-a" + + tags = { + "${google_tags_tag_key.key_1.namespaced_name}" = google_tags_tag_value.key_1_val_1.short_name + } + + depends_on = [ + google_tags_tag_value.key_1_val_1, + google_tags_tag_value.key_1_val_2, + ] +} +`, context) +} + +func dummyResourceState() *terraform.ResourceState { + return &terraform.ResourceState{ + Primary: &terraform.InstanceState{}, + } +} + +func dummyResourceStateWithLocation(loc string) *terraform.ResourceState { + return &terraform.ResourceState{ + Primary: &terraform.InstanceState{ + Attributes: map[string]string{ + "location": loc, + }, + }, + } +} + +func testAccCheckBindingValueViaAPI_Direct(t *testing.T, location, encodedResourceName, keyNamespacedName, expectedValueShortName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + config := acctest.GoogleProviderConfig(t) + basePath := "{{"{{"}}TagsLocationBasePath{{"}}"}}" + if location == "global" { + basePath = "{{"{{"}}TagsBasePath{{"}}"}}" + } + apiURL, err := tpgresource.ReplaceVarsForTest(config, dummyResourceState(), fmt.Sprintf("%slocations/%s/tagBindingCollections/%s", basePath, location, encodedResourceName)) + if err != nil { + return err + } + res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "GET", + RawURL: apiURL, + Headers: make(http.Header), + }) + if err != nil { + return fmt.Errorf("Error reading TagBindingCollection via API: %s", err) + } + tagsMap, ok := res["tags"].(map[string]interface{}) + if !ok { + return fmt.Errorf("API response does not contain 'tags' map") + } + currentValue, ok := tagsMap[keyNamespacedName].(string) + if !ok || currentValue != expectedValueShortName { + return fmt.Errorf("TagValue mismatch via direct API GET: expected %s, got %s", expectedValueShortName, currentValue) + } + return nil + } +} + +func testAccCheckTagADeleted(t *testing.T, contextMap map[string]interface{}) resource.TestCheckFunc { + return func(s *terraform.State) error { + config := acctest.GoogleProviderConfig(t) + orgId := contextMap["org_id"].(string) + keyANamespaced := fmt.Sprintf("%s/%s", orgId, contextMap["key_a_short"]) + + projectResource, ok := s.RootModule().Resources["google_project.project"] + if !ok { + return fmt.Errorf("Resource 'google_project.project' not found in state") + } + projectNumber := projectResource.Primary.Attributes["number"] + + resourceName := fmt.Sprintf("//cloudresourcemanager.googleapis.com/projects/%s", projectNumber) + encodedResourceName := url.PathEscape(resourceName) + location := "global" + + headers := make(http.Header) + apiURL, err := tpgresource.ReplaceVarsForTest(config, dummyResourceState(), fmt.Sprintf("{{"{{"}}TagsBasePath{{"}}"}}locations/%s/tagBindingCollections/%s", location, encodedResourceName)) + if err != nil { + return err + } + + res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "GET", + RawURL: apiURL, + Headers: headers, + }) + + if err != nil { + if transport_tpg.IsGoogleApiErrorWithCode(err, 404) { + return nil + } + return fmt.Errorf("Error reading TagBindingCollection: %s", err) + } + + if res != nil && res["tags"] != nil { + activeTagsIFace, ok := res["tags"].(map[string]interface{}) + if ok { + if _, exists := activeTagsIFace[keyANamespaced]; exists { + return fmt.Errorf("Tag %s should have been removed, but still exists in the collection", keyANamespaced) + } + } + } + return nil + } +} + +// testAccCheckBindingUpdatedExternally performs the out-of-band API call to change the TagValue. +func testAccCheckBindingUpdatedExternally(t *testing.T, contextMap map[string]interface{}, keyNamespacedName, newValueShortName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + config := acctest.GoogleProviderConfig(t) + + projectResource, ok := s.RootModule().Resources["google_project.project"] + if !ok { + return fmt.Errorf("Resource 'google_project.project' not found in state") + } + projectNumber := projectResource.Primary.Attributes["number"] + + resourceName := fmt.Sprintf("//cloudresourcemanager.googleapis.com/projects/%s", projectNumber) + encodedResourceName := url.PathEscape(resourceName) + location := "global" + + apiURL, err := tpgresource.ReplaceVarsForTest(config, dummyResourceState(), fmt.Sprintf("{{"{{"}}TagsBasePath{{"}}"}}locations/%s/tagBindingCollections/%s", location, encodedResourceName)) + if err != nil { + return err + } + + body := map[string]interface{}{ + "full_resource_name": resourceName, + "tags": map[string]string{ + keyNamespacedName: newValueShortName, + }, + } + + _, err = transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "PATCH", + RawURL: apiURL, + Body: body, + Headers: make(http.Header), + }) + + if err != nil { + return fmt.Errorf("Error updating TagBindingCollection via API: %s", err) + } + t.Logf("Successfully updated binding for %s to %s via API", keyNamespacedName, newValueShortName) + return nil + } +} + +// testAccCheckBindingDeletedExternally performs the out-of-band API call to delete the TagBinding. +func testAccCheckBindingDeletedExternally(t *testing.T, contextMap map[string]interface{}, keyNamespacedName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + config := acctest.GoogleProviderConfig(t) + + projectResource, ok := s.RootModule().Resources["google_project.project"] + if !ok { + return fmt.Errorf("Resource 'google_project.project' not found in state") + } + projectNumber := projectResource.Primary.Attributes["number"] + + resourceName := fmt.Sprintf("//cloudresourcemanager.googleapis.com/projects/%s", projectNumber) + encodedResourceName := url.PathEscape(resourceName) + location := "global" + + // 1. Get current TagBindingCollection to find Etag + apiURL, err := tpgresource.ReplaceVarsForTest(config, dummyResourceState(), fmt.Sprintf("{{"{{"}}TagsBasePath{{"}}"}}locations/%s/tagBindingCollections/%s", location, encodedResourceName)) + if err != nil { + return err + } + res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "GET", + RawURL: apiURL, + UserAgent: config.UserAgent, + }) + if err != nil { + return fmt.Errorf("Error fetching parent TagBindingCollection before deletion: %v", err) + } + + currentEtag := res["etag"].(string) + + body := map[string]interface{}{ + "full_resource_name": resourceName, + "etag": currentEtag, + "tags": make(map[string]interface{}), + } + + _, err = transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "PATCH", + RawURL: apiURL + "?updateMask=*", + Body: body, + Headers: make(http.Header), + }) + + if err != nil { + return fmt.Errorf("Error deleting TagBindingCollection via API: %s", err) + } + t.Logf("Successfully deleted binding for %s externally via API", keyNamespacedName) + return nil + } +} + +// testAccCheckBindingValueViaAPI checks the current TagValue for a key by calling the GET API. +func testAccCheckBindingValueViaAPI(t *testing.T, contextMap map[string]interface{}, keyNamespacedName, expectedValueShortName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + config := acctest.GoogleProviderConfig(t) + + projectResource, ok := s.RootModule().Resources["google_project.project"] + if !ok { + return fmt.Errorf("Resource 'google_project.project' not found in state") + } + projectNumber := projectResource.Primary.Attributes["number"] + + resourceName := fmt.Sprintf("//cloudresourcemanager.googleapis.com/projects/%s", projectNumber) + encodedResourceName := url.PathEscape(resourceName) + location := "global" + + apiURL, err := tpgresource.ReplaceVarsForTest(config, dummyResourceState(), fmt.Sprintf("{{"{{"}}TagsBasePath{{"}}"}}locations/%s/tagBindingCollections/%s", location, encodedResourceName)) + if err != nil { + return err + } + + res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "GET", + RawURL: apiURL, + Headers: make(http.Header), + }) + + if err != nil { + return fmt.Errorf("Error reading TagBindingCollection via API: %s", err) + } + + tagsMap, ok := res["tags"].(map[string]interface{}) + if !ok { + return fmt.Errorf("API response does not contain 'tags' map or it's not the expected type") + } + + currentValue, ok := tagsMap[keyNamespacedName].(string) + if !ok { + return fmt.Errorf("Tag key %s not found in the collection read from API", keyNamespacedName) + } + + if currentValue != expectedValueShortName { + return fmt.Errorf("Expected tag value for %s to be %s, but got %s from API", keyNamespacedName, expectedValueShortName, currentValue) + } + + t.Logf("Confirmed via API that binding for %s is %s", keyNamespacedName, currentValue) + return nil + } +} + +// Unit Tests + +func TestResourceTagsTagBindingCollectionImport(t *testing.T) { + // This test function is defined in the package 'tags', so we can call the function directly. + importer := tags.ResourceTagsTagBindingCollection().Importer + if importer == nil || importer.StateContext == nil { + t.Fatalf("Importer not configured for google_tags_tag_binding_collection") + } + StateContext := importer.StateContext + + cases := []struct { + Name string + ImportID string + ExpectedID string + ExpectedLoc string + ExpectedResourceName string + ExpectError bool + }{ + { + Name: "StandardImportProject", + ImportID: "locations/global/tagBindingCollections/%2F%2Fcloudresourcemanager.googleapis.com%2Fprojects%2F123", + ExpectedID: "locations/global/tagBindingCollections/%2F%2Fcloudresourcemanager.googleapis.com%2Fprojects%2F123", + ExpectedLoc: "global", + ExpectedResourceName: "//cloudresourcemanager.googleapis.com/projects/123", + ExpectError: false, + }, + { + Name: "ZonalImportComputeInstance", + ImportID: "locations/us-central1-a/tagBindingCollections/%2F%2Fcompute.googleapis.com/projects%2F123%2Fzones%2Fus-central1-a%2Finstances%2F456", + ExpectedID: "locations/us-central1-a/tagBindingCollections/%2F%2Fcompute.googleapis.com/projects%2F123%2Fzones%2Fus-central1-a%2Finstances%2F456", + ExpectedLoc: "us-central1-a", + ExpectedResourceName: "//compute.googleapis.com/projects/123/zones/us-central1-a/instances/456", + ExpectError: false, + }, + { + Name: "InvalidFormatMissingLocationPrefix", + ImportID: "global/tagBindingCollections/%2F%2Fcloudresourcemanager.googleapis.com%2Fprojects%2F123", + ExpectError: true, + }, + { + Name: "InvalidFormatMissingCollectionKeyword", + ImportID: "locations/global/%2F%2Fcloudresourcemanager.googleapis.com%2Fprojects%2F123", + ExpectError: true, + }, + { + Name: "InvalidEncodedResourceName", + ImportID: "locations/global/tagBindingCollections/%invalid", + ExpectError: true, + }, + { + Name: "EmptyResourceName", + ImportID: "locations/global/tagBindingCollections/", + ExpectError: true, + }, + } + + for _, tc := range cases { + t.Run(tc.Name, func(t *testing.T) { + d := schema.TestResourceDataRaw(t, tags.ResourceTagsTagBindingCollection().Schema, nil) + d.SetId(tc.ImportID) + + newData, err := StateContext(context.Background(), d, nil) + + if tc.ExpectError { + if err == nil { + t.Errorf("Expected error but got none") + } + return + } + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if len(newData) != 1 { + t.Fatalf("Expected 1 ResourceData to be returned, got %d", len(newData)) + } + + data := newData[0] + if data.Id() != tc.ExpectedID { + t.Errorf("Expected ID %q, got %q", tc.ExpectedID, data.Id()) + } + if got := data.Get("location").(string); got != tc.ExpectedLoc { + t.Errorf("Expected location %q, got %q", tc.ExpectedLoc, got) + } + if got := data.Get("full_resource_name").(string); got != tc.ExpectedResourceName { + t.Errorf("Expected full_resource_name %q, got %q", tc.ExpectedResourceName, got) + } + }) + } +} + +func testAccTagsTagBindingCollection_locationDefault(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_project" "project" { + provider = google-beta + project_id = "%{project_id}" + name = "%{project_id}" + org_id = "%{org_id}" + deletion_policy = "DELETE" +} + +resource "google_tags_tag_key" "env_key" { + provider = google-beta + parent = "organizations/%{org_id}" + short_name = "%{env_key_short_name}" +} +resource "google_tags_tag_value" "env_prod_value" { + provider = google-beta + parent = google_tags_tag_key.env_key.id + short_name = "%{env_prod_value_short_name}" +} + +resource "google_tags_tag_key" "cost_center_key" { + provider = google-beta + parent = "organizations/%{org_id}" + short_name = "%{cc_key_short_name}" +} +resource "google_tags_tag_value" "cc_marketing_value" { + provider = google-beta + parent = google_tags_tag_key.cost_center_key.id + short_name = "%{cc_marketing_value_short_name}" +} + +resource "google_tags_tag_binding_collection" "location_default" { + provider = google-beta + full_resource_name = "//cloudresourcemanager.googleapis.com/projects/${google_project.project.number}" + # location is omitted, should default to "global" + tags = { + "${google_tags_tag_key.env_key.namespaced_name}" = google_tags_tag_value.env_prod_value.short_name + "${google_tags_tag_key.cost_center_key.namespaced_name}" = google_tags_tag_value.cc_marketing_value.short_name + } + + depends_on = [ + google_tags_tag_value.env_prod_value, + google_tags_tag_value.cc_marketing_value, + ] +} +`, context) +} + +func testAccTagsTagBindingCollection_mixConfig(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_project" "project" { + provider = google-beta + project_id = "%{project_id}" + name = "%{project_id}" + org_id = "%{org_id}" + deletion_policy = "DELETE" +} + +resource "google_tags_tag_key" "curated_key" { + provider = google-beta + parent = "organizations/%{org_id}" + short_name = "%{curated_key_short}" +} +resource "google_tags_tag_value" "curated_val" { + provider = google-beta + parent = google_tags_tag_key.curated_key.id + short_name = "%{curated_val_short}" +} + +resource "google_tags_tag_key" "dynamic_key" { + provider = google-beta + parent = "organizations/%{org_id}" + short_name = "%{dynamic_key_short}" + allowed_values_regex = "^[a-z0-9]+$" +} + +resource "google_tags_tag_binding_collection" "mix" { + provider = google-beta + full_resource_name = "//cloudresourcemanager.googleapis.com/projects/${google_project.project.number}" + location = "global" + tags = { + "${google_tags_tag_key.curated_key.namespaced_name}" = google_tags_tag_value.curated_val.short_name + "${google_tags_tag_key.dynamic_key.namespaced_name}" = "%{dynamic_bind_val}" + } + + depends_on = [ + google_tags_tag_value.curated_val, + ] +} +`, context) +} + +func testAccTagsTagBindingCollection_restorationConfig(context map[string]interface{}) string { + return acctest.Nprintf(` +data "google_compute_image" "my_image" { + provider = google-beta + family = "debian-11" + project = "debian-cloud" +} + +resource "google_compute_instance" "instance" { + provider = google-beta + name = "%{instance_name}" + machine_type = "e2-micro" + zone = "%{zone}" + + boot_disk { + initialize_params { + image = data.google_compute_image.my_image.self_link + } + } + + network_interface { + network = "default" + } +} + +resource "google_tags_tag_key" "key" { + provider = google-beta + parent = "organizations/%{org_id}" + short_name = "%{key_short}" +} + +resource "google_tags_tag_value" "val" { + provider = google-beta + parent = google_tags_tag_key.key.id + short_name = "%{val_short}" +} + +resource "google_tags_tag_binding_collection" "rest" { + provider = google-beta + full_resource_name = "//compute.googleapis.com/projects/%{project_id}/zones/%{zone}/instances/${google_compute_instance.instance.instance_id}" + location = "us-central1-a" + + tags = { + "${google_tags_tag_key.key.namespaced_name}" = google_tags_tag_value.val.short_name + } + + depends_on = [ + google_tags_tag_value.val, + ] +} +`, context) +} + +// TODO: Add more unit tests for Upsert and Delete logic, mocking API calls. + +func TestAccTagsTagBindingCollection_regional_basic(t *testing.T) { + t.Parallel() + + orgId := envvar.GetTestOrgFromEnv(t) + context := map[string]interface{}{ + "org_id": orgId, + "key_short_name": "tf-test-reg-" + acctest.RandString(t, 6), + "value_short_name": "tf-test-val-" + acctest.RandString(t, 6), + "project_id": envvar.GetTestProjectFromEnv(), + "instance_name": "tf-test-vm-" + acctest.RandString(t, 10), + "zone": "us-central1-a", + } + + keyNamespaced := fmt.Sprintf("%s/%s", orgId, context["key_short_name"]) + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderBetaFactories(t), + Steps: []resource.TestStep{ + // Step 1: Create regional TagBindingCollection on Compute VM + { + Config: testAccTagsTagBindingCollection_regional_basic(context), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.regional", "tags.%", "1"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.regional", "active_tags.%", "1"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.regional", fmt.Sprintf("tags.%s", keyNamespaced), context["value_short_name"].(string)), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.regional", fmt.Sprintf("active_tags.%s", keyNamespaced), context["value_short_name"].(string)), + resource.TestMatchResourceAttr("google_tags_tag_binding_collection.regional", "id", regexp.MustCompile("locations/us-central1-a/tagBindingCollections/.+")), + resource.TestMatchResourceAttr("google_tags_tag_binding_collection.regional", "name", regexp.MustCompile("locations/us-central1-a/tagBindingCollections/.+")), + ), + }, + // Step 2: Import + { + ResourceName: "google_tags_tag_binding_collection.regional", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"full_resource_name", "location", "tags"}, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.regional", "tags.%", "0"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.regional", "active_tags.%", "1"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.regional", "location", "us-central1-a"), + ), + }, + }, + }) +} + +func testAccTagsTagBindingCollection_regional_basic(context map[string]interface{}) string { + return acctest.Nprintf(` +data "google_compute_image" "my_image" { + provider = google-beta + family = "debian-11" + project = "debian-cloud" +} + +resource "google_compute_instance" "instance" { + provider = google-beta + name = "%{instance_name}" + machine_type = "e2-micro" + zone = "%{zone}" + + boot_disk { + initialize_params { + image = data.google_compute_image.my_image.self_link + } + } + + network_interface { + network = "default" + } +} + +resource "google_tags_tag_key" "key" { + provider = google-beta + parent = "organizations/%{org_id}" + short_name = "%{key_short_name}" +} + +resource "google_tags_tag_value" "value" { + provider = google-beta + parent = google_tags_tag_key.key.id + short_name = "%{value_short_name}" +} + +resource "google_tags_tag_binding_collection" "regional" { + provider = google-beta + full_resource_name = "//compute.googleapis.com/projects/%{project_id}/zones/%{zone}/instances/${google_compute_instance.instance.instance_id}" + location = "us-central1-a" + + tags = { + "${google_tags_tag_key.key.namespaced_name}" = google_tags_tag_value.value.short_name + } +} +`, context) +} + +func TestAccTagsTagBindingCollection_forceNew(t *testing.T) { + t.Parallel() + + orgId := envvar.GetTestOrgFromEnv(t) + context := map[string]interface{}{ + "org_id": orgId, + "key_short_name": "tf-test-fn-" + acctest.RandString(t, 6), + "value_short_name": "tf-test-val-" + acctest.RandString(t, 6), + "project_id": envvar.GetTestProjectFromEnv(), + "instance_name": "tf-test-vm-fn-" + acctest.RandString(t, 10), + "zone_1": "us-central1-a", + "zone_2": "us-central1-b", + } + + keyNamespaced := fmt.Sprintf("%s/%s", orgId, context["key_short_name"]) + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderBetaFactories(t), + Steps: []resource.TestStep{ + // Step 1: Create TagBindingCollection on VM in zone_1 + { + Config: testAccTagsTagBindingCollection_forceNewConfig1(context), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.fn", "tags.%", "1"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.fn", fmt.Sprintf("tags.%s", keyNamespaced), context["value_short_name"].(string)), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.fn", "location", "us-central1-a"), + ), + }, + // Step 2: Update configuration to target VM in zone_2! This must trigger a perfect ForceNew replacement! + { + Config: testAccTagsTagBindingCollection_forceNewConfig2(context), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.fn", "tags.%", "1"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.fn", fmt.Sprintf("tags.%s", keyNamespaced), context["value_short_name"].(string)), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.fn", "location", "us-central1-b"), + ), + }, + }, + }) +} + +func testAccTagsTagBindingCollection_forceNewConfig1(context map[string]interface{}) string { + return acctest.Nprintf(` +data "google_compute_image" "my_image" { + provider = google-beta + family = "debian-11" + project = "debian-cloud" +} + +resource "google_compute_instance" "vm1" { + provider = google-beta + name = "%{instance_name}-1" + machine_type = "e2-micro" + zone = "%{zone_1}" + boot_disk { + initialize_params { + image = data.google_compute_image.my_image.self_link + } + } + network_interface { + network = "default" + } +} + +resource "google_compute_instance" "vm2" { + provider = google-beta + name = "%{instance_name}-2" + machine_type = "e2-micro" + zone = "%{zone_2}" + boot_disk { + initialize_params { + image = data.google_compute_image.my_image.self_link + } + } + network_interface { + network = "default" + } +} + +resource "google_tags_tag_key" "key" { + provider = google-beta + parent = "organizations/%{org_id}" + short_name = "%{key_short_name}" +} + +resource "google_tags_tag_value" "value" { + provider = google-beta + parent = google_tags_tag_key.key.id + short_name = "%{value_short_name}" +} + +resource "google_tags_tag_binding_collection" "fn" { + provider = google-beta + full_resource_name = "//compute.googleapis.com/projects/%{project_id}/zones/%{zone_1}/instances/${google_compute_instance.vm1.instance_id}" + location = "us-central1-a" + + tags = { + "${google_tags_tag_key.key.namespaced_name}" = google_tags_tag_value.value.short_name + } +} +`, context) +} + +func testAccTagsTagBindingCollection_forceNewConfig2(context map[string]interface{}) string { + return acctest.Nprintf(` +data "google_compute_image" "my_image" { + provider = google-beta + family = "debian-11" + project = "debian-cloud" +} + +resource "google_compute_instance" "vm1" { + provider = google-beta + name = "%{instance_name}-1" + machine_type = "e2-micro" + zone = "%{zone_1}" + boot_disk { + initialize_params { + image = data.google_compute_image.my_image.self_link + } + } + network_interface { + network = "default" + } +} + +resource "google_compute_instance" "vm2" { + provider = google-beta + name = "%{instance_name}-2" + machine_type = "e2-micro" + zone = "%{zone_2}" + boot_disk { + initialize_params { + image = data.google_compute_image.my_image.self_link + } + } + network_interface { + network = "default" + } +} + +resource "google_tags_tag_key" "key" { + provider = google-beta + parent = "organizations/%{org_id}" + short_name = "%{key_short_name}" +} + +resource "google_tags_tag_value" "value" { + provider = google-beta + parent = google_tags_tag_key.key.id + short_name = "%{value_short_name}" +} + +resource "google_tags_tag_binding_collection" "fn" { + provider = google-beta + full_resource_name = "//compute.googleapis.com/projects/%{project_id}/zones/%{zone_2}/instances/${google_compute_instance.vm2.instance_id}" + location = "us-central1-b" + + tags = { + "${google_tags_tag_key.key.namespaced_name}" = google_tags_tag_value.value.short_name + } +} +`, context) +} + +func TestAccTagsTagBindingCollection_regionalMismatch(t *testing.T) { + t.Parallel() + + orgId := envvar.GetTestOrgFromEnv(t) + context := map[string]interface{}{ + "org_id": orgId, + "key_short_name": "tf-test-err-" + acctest.RandString(t, 6), + "value_short_name": "tf-test-val-" + acctest.RandString(t, 6), + "project_id": envvar.GetTestProjectFromEnv(), + "instance_name": "tf-test-vm-err-" + acctest.RandString(t, 10), + "zone": "us-central1-a", + } + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderBetaFactories(t), + Steps: []resource.TestStep{ + // Attempt to bind to a Zonal Compute VM but pass location = "global" + { + Config: testAccTagsTagBindingCollection_regionalMismatchConfig(context), + ExpectError: regexp.MustCompile("Must be a valid One Platform resource name of a tag-compatible global resource"), + }, + }, + }) +} + +func testAccTagsTagBindingCollection_regionalMismatchConfig(context map[string]interface{}) string { + return acctest.Nprintf(` +data "google_compute_image" "my_image" { + provider = google-beta + family = "debian-11" + project = "debian-cloud" +} + +resource "google_compute_instance" "instance" { + provider = google-beta + name = "%{instance_name}" + machine_type = "e2-micro" + zone = "%{zone}" + boot_disk { + initialize_params { + image = data.google_compute_image.my_image.self_link + } + } + network_interface { + network = "default" + } +} + +resource "google_tags_tag_key" "key" { + provider = google-beta + parent = "organizations/%{org_id}" + short_name = "%{key_short_name}" +} + +resource "google_tags_tag_value" "value" { + provider = google-beta + parent = google_tags_tag_key.key.id + short_name = "%{value_short_name}" +} + +resource "google_tags_tag_binding_collection" "err" { + provider = google-beta + full_resource_name = "//compute.googleapis.com/projects/%{project_id}/zones/%{zone}/instances/${google_compute_instance.instance.instance_id}" + location = "global" + + tags = { + "${google_tags_tag_key.key.namespaced_name}" = google_tags_tag_value.value.short_name + } +} +`, context) +} + +func testAccCheckBindingUpdatedExternally_VM(t *testing.T, contextMap map[string]interface{}, keyNamespacedName, newValueShortName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + config := acctest.GoogleProviderConfig(t) + vmResource := s.RootModule().Resources["google_compute_instance.instance"] + if vmResource == nil { + return fmt.Errorf("Resource 'google_compute_instance.instance' not found in state") + } + instanceId := vmResource.Primary.Attributes["instance_id"] + projectId := contextMap["project_id"].(string) + zone := contextMap["zone"].(string) + + resourceName := fmt.Sprintf("//compute.googleapis.com/projects/%s/zones/%s/instances/%s", projectId, zone, instanceId) + encodedResourceName := url.PathEscape(resourceName) + location := zone + + apiURL, err := tpgresource.ReplaceVarsForTest(config, dummyResourceStateWithLocation(location), fmt.Sprintf("{{"{{"}}TagsLocationBasePath{{"}}"}}locations/%s/tagBindingCollections/%s", location, encodedResourceName)) + if err != nil { + return err + } + apiURL, err = tpgresource.ReplaceVarsForTest(config, dummyResourceStateWithLocation(location), apiURL) + if err != nil { + return err + } + + body := map[string]interface{}{ + "full_resource_name": resourceName, + "tags": map[string]string{ + keyNamespacedName: newValueShortName, + }, + } + + _, err = transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "PATCH", + RawURL: apiURL + "?updateMask=*", + Body: body, + Headers: make(http.Header), + }) + if err != nil { + return fmt.Errorf("Error updating TagBindingCollection via API: %s", err) + } + t.Logf("Successfully updated physical binding for %s to %s via API", keyNamespacedName, newValueShortName) + return nil + } +} + +func testAccCheckBindingDeletedExternally_VM(t *testing.T, contextMap map[string]interface{}, keyNamespacedName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + config := acctest.GoogleProviderConfig(t) + vmResource := s.RootModule().Resources["google_compute_instance.instance"] + if vmResource == nil { + return fmt.Errorf("Resource 'google_compute_instance.instance' not found in state") + } + instanceId := vmResource.Primary.Attributes["instance_id"] + projectId := contextMap["project_id"].(string) + zone := contextMap["zone"].(string) + + resourceName := fmt.Sprintf("//compute.googleapis.com/projects/%s/zones/%s/instances/%s", projectId, zone, instanceId) + encodedResourceName := url.PathEscape(resourceName) + location := zone + + apiURL, err := tpgresource.ReplaceVarsForTest(config, dummyResourceStateWithLocation(location), fmt.Sprintf("{{"{{"}}TagsLocationBasePath{{"}}"}}locations/%s/tagBindingCollections/%s", location, encodedResourceName)) + if err != nil { + return err + } + apiURL, err = tpgresource.ReplaceVarsForTest(config, dummyResourceStateWithLocation(location), apiURL) + if err != nil { + return err + } + res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "GET", + RawURL: apiURL, + }) + if err != nil { + return fmt.Errorf("Error fetching parent TagBindingCollection before deletion: %v", err) + } + + currentEtag := res["etag"].(string) + + body := map[string]interface{}{ + "full_resource_name": resourceName, + "etag": currentEtag, + "tags": make(map[string]interface{}), + } + + _, err = transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "PATCH", + RawURL: apiURL + "?updateMask=*", + Body: body, + Headers: make(http.Header), + }) + if err != nil { + return fmt.Errorf("Error deleting TagBindingCollection via API: %s", err) + } + t.Logf("Successfully deleted physical binding for %s externally via API", keyNamespacedName) + return nil + } +} + +func testAccCheckBindingValueViaAPI_VM(t *testing.T, contextMap map[string]interface{}, keyNamespacedName, expectedValueShortName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + config := acctest.GoogleProviderConfig(t) + vmResource := s.RootModule().Resources["google_compute_instance.instance"] + if vmResource == nil { + return fmt.Errorf("Resource 'google_compute_instance.instance' not found in state") + } + instanceId := vmResource.Primary.Attributes["instance_id"] + projectId := contextMap["project_id"].(string) + zone := contextMap["zone"].(string) + + resourceName := fmt.Sprintf("//compute.googleapis.com/projects/%s/zones/%s/instances/%s", projectId, zone, instanceId) + encodedResourceName := url.PathEscape(resourceName) + location := zone + + apiURL, err := tpgresource.ReplaceVarsForTest(config, dummyResourceStateWithLocation(location), fmt.Sprintf("{{"{{"}}TagsLocationBasePath{{"}}"}}locations/%s/tagBindingCollections/%s", location, encodedResourceName)) + if err != nil { + return err + } + apiURL, err = tpgresource.ReplaceVarsForTest(config, dummyResourceStateWithLocation(location), apiURL) + if err != nil { + return err + } + + res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "GET", + RawURL: apiURL, + }) + if err != nil { + return fmt.Errorf("Error reading TagBindingCollection via API: %s", err) + } + var tagsMap map[string]interface{} + if res["tags"] != nil { + tagsMap = res["tags"].(map[string]interface{}) + } + var currentValue string + if tagsMap != nil { + currentValue, _ = tagsMap[keyNamespacedName].(string) + } + if currentValue != expectedValueShortName { + return fmt.Errorf("TagValue mismatch via direct API GET: expected %q, got %q", expectedValueShortName, currentValue) + } + return nil + } +} + +func TestAccTagsTagBindingCollection_nonAuthoritative(t *testing.T) { + t.Parallel() + + orgId := envvar.GetTestOrgFromEnv(t) + context := map[string]interface{}{ + "org_id": orgId, + "project_id": envvar.GetTestProjectFromEnv(), + "instance_name": "tf-test-vm-nonauth-" + acctest.RandString(t, 10), + "zone": "us-central1-a", + "key_1_short": "tf-test-k1-" + acctest.RandString(t, 6), + "val_1_short": "tf-test-v1-" + acctest.RandString(t, 6), // Managed by TF + "key_2_short": "tf-test-k2-" + acctest.RandString(t, 6), + "val_2_short": "tf-test-v2-" + acctest.RandString(t, 6), // Attached externally + } + + key1Namespaced := fmt.Sprintf("%s/%s", orgId, context["key_1_short"]) + key2Namespaced := fmt.Sprintf("%s/%s", orgId, context["key_2_short"]) + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderBetaFactories(t), + Steps: []resource.TestStep{ + // Step 1: Create the collection with 1 managed tag (key_1 = val_1) + { + Config: testAccTagsTagBindingCollection_nonAuthoritativeConfig(context), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.test_nonauth", "tags.%", "1"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.test_nonauth", "active_tags.%", "1"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.test_nonauth", fmt.Sprintf("tags.%s", key1Namespaced), context["val_1_short"].(string)), + ), + }, + // Step 2: Intermediate PlanOnly step that attaches key_2 = val_2 externally (out-of-band) + { + PlanOnly: true, + ExpectNonEmptyPlan: false, + Config: testAccTagsTagBindingCollection_nonAuthoritativeConfig(context), + Check: resource.ComposeTestCheckFunc( + testAccCheckBindingAddedExternally_VM(t, context, "google_tags_tag_value.key_2_val_2"), + testAccCheckBindingValueViaAPI_VM(t, context, key2Namespaced, context["val_2_short"].(string)), + ), + }, + // Step 3: Run plan again. It should show NO CHANGES (empty plan), + // proving that Terraform does not attempt to delete the externally attached key_2! + { + Config: testAccTagsTagBindingCollection_nonAuthoritativeConfig(context), + PlanOnly: true, + ExpectNonEmptyPlan: false, + Check: resource.ComposeTestCheckFunc( + // active_tags should now show both tags (managed + external) + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.test_nonauth", "active_tags.%", "2"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.test_nonauth", fmt.Sprintf("active_tags.%s", key1Namespaced), context["val_1_short"].(string)), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.test_nonauth", fmt.Sprintf("active_tags.%s", key2Namespaced), context["val_2_short"].(string)), + // tags should still only show the 1 managed tag + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.test_nonauth", "tags.%", "1"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.test_nonauth", fmt.Sprintf("tags.%s", key1Namespaced), context["val_1_short"].(string)), + ), + }, + }, + }) +} + +func testAccTagsTagBindingCollection_nonAuthoritativeConfig(context map[string]interface{}) string { + return acctest.Nprintf(` +data "google_compute_image" "my_image" { + provider = google-beta + family = "debian-11" + project = "debian-cloud" +} + +resource "google_compute_instance" "instance" { + provider = google-beta + name = "%{instance_name}" + machine_type = "e2-micro" + zone = "%{zone}" + + boot_disk { + initialize_params { + image = data.google_compute_image.my_image.self_link + } + } + + network_interface { + network = "default" + } +} + +resource "google_tags_tag_key" "key_1" { + provider = google-beta + parent = "organizations/%{org_id}" + short_name = "%{key_1_short}" +} + +resource "google_tags_tag_value" "key_1_val_1" { + provider = google-beta + parent = google_tags_tag_key.key_1.id + short_name = "%{val_1_short}" +} + +resource "google_tags_tag_key" "key_2" { + provider = google-beta + parent = "organizations/%{org_id}" + short_name = "%{key_2_short}" +} + +resource "google_tags_tag_value" "key_2_val_2" { + provider = google-beta + parent = google_tags_tag_key.key_2.id + short_name = "%{val_2_short}" +} + +resource "google_tags_tag_binding_collection" "test_nonauth" { + provider = google-beta + full_resource_name = "//compute.googleapis.com/projects/%{project_id}/zones/%{zone}/instances/${google_compute_instance.instance.instance_id}" + location = "us-central1-a" + + tags = { + "${google_tags_tag_key.key_1.namespaced_name}" = google_tags_tag_value.key_1_val_1.short_name + } + + depends_on = [ + google_tags_tag_value.key_1_val_1, + google_tags_tag_value.key_2_val_2, + ] +} +`, context) +} + +// testAccCheckBindingAddedExternally_VM performs an out-of-band API call to create an individual TagBinding. +// This simulates another client attaching a tag to the same resource. +func testAccCheckBindingAddedExternally_VM(t *testing.T, contextMap map[string]interface{}, tagValueResourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + config := acctest.GoogleProviderConfig(t) + + // Resolve tag value ID from state + tagValueRes, ok := s.RootModule().Resources[tagValueResourceName] + if !ok { + return fmt.Errorf("Resource %q not found in state", tagValueResourceName) + } + tagValueId := tagValueRes.Primary.ID // This is "tagValues/12345" + + vmResource := s.RootModule().Resources["google_compute_instance.instance"] + if vmResource == nil { + return fmt.Errorf("Resource 'google_compute_instance.instance' not found in state") + } + instanceId := vmResource.Primary.Attributes["instance_id"] + projectId := contextMap["project_id"].(string) + zone := contextMap["zone"].(string) + + resourceName := fmt.Sprintf("//compute.googleapis.com/projects/%s/zones/%s/instances/%s", projectId, zone, instanceId) + + // Create a single TagBinding via the API + apiURL := "https://cloudresourcemanager.googleapis.com/v3/tagBindings" + body := map[string]interface{}{ + "parent": resourceName, + "tagValue": tagValueId, + } + + _, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "POST", + RawURL: apiURL, + Body: body, + Headers: make(http.Header), + }) + + if err != nil { + return fmt.Errorf("Error creating external TagBinding via API: %s", err) + } + t.Logf("Successfully created external binding for %s to %s via API", resourceName, tagValueId) + return nil + } +} + +{{- end }} + diff --git a/mmv1/third_party/terraform/website/docs/r/tags_tag_binding_collection.html.markdown.tmpl b/mmv1/third_party/terraform/website/docs/r/tags_tag_binding_collection.html.markdown.tmpl new file mode 100644 index 000000000000..4fb9bf45ae9f --- /dev/null +++ b/mmv1/third_party/terraform/website/docs/r/tags_tag_binding_collection.html.markdown.tmpl @@ -0,0 +1,127 @@ +{{- if ne $.TargetVersionName "ga" }} +--- +subcategory: "Tags" +description: |- + A TagBindingCollection represents a collection of tag bindings directly bound to a cloud resource. +--- + +# google_tags_tag_binding_collection + +A TagBindingCollection represents a collection of tag bindings directly bound to a cloud resource. + +> **Warning:** `google_tags_tag_binding_collection` is designed to perform non-authoritative bulk management of tags. It **cannot** be used in conjunction with individual `google_tags_tag_binding` or `google_tags_location_tag_binding` resources operating on the exact same parent resource. Doing so will cause overlapping state management, double adoptions, and unpredictable infrastructure convergence. + +To get more information about TagBindingCollection, see: + +* [API documentation](https://cloud.google.com/resource-manager/reference/rest/v3/locations.tagBindingCollections) +* How-to Guides + * [Official Documentation](https://cloud.google.com/resource-manager/docs/tags/tags-creating-and-managing) + +## Example Usage - Cloud Run Service + +To bind tags to a Cloud Run service: + +```hcl +resource "google_project" "project" { + project_id = "project_id" + name = "project_id" + org_id = "123456789" +} + +resource "google_tags_tag_key" "key1" { + parent = "organizations/123456789" + short_name = "keyname1" + description = "For keyname1 resources." +} + +resource "google_tags_tag_value" "value1" { + parent = google_tags_tag_key.key1.id + short_name = "valuename1" + description = "For valuename1 resources." +} + +resource "google_tags_tag_key" "key2" { + parent = "organizations/123456789" + short_name = "dynamic_keyname" + description = "For dynamic keyname resources." + allowed_values_regex = "^[a-z0-9]+$" +} + +resource "google_tags_tag_binding_collection" "bindingcollection" { + full_resource_name = "//run.googleapis.com/projects/${data.google_project.project.number}/locations/${google_cloud_run_service.default.location}/services/${google_cloud_run_service.default.name}" + location = "us-central1" + tags = { + # Format: "{TagKey.namespaced_name}" = "{TagValue.short_name}" + "${google_tags_tag_key.key1.namespaced_name}" = google_tags_tag_value.value1.short_name + "${google_tags_tag_key.key2.namespaced_name}" = "dynamicvalue123" + } +} +``` + +## Argument Reference + +The following arguments are supported: + + +* `full_resource_name` - + (Required) + The full resource name of the resource to which the tags are bound. E.g. `//cloudresourcemanager.googleapis.com/projects/123` + +* `tags` - + (Required) + A map of tag keys to values directly bound to this resource, specified in namespaced name format. E.g. `"123/environment": "production"`. Keys must be namespaced names of TagKeys, and values must be short names of TagValues. This field is non-authoritative. Terraform will only manage the precise tags present in this map. + +* `location` - + (Optional) + The location of the target resource. E.g. `"global"`, `"us-central1"`, `"us-east2-c"`. Defaults to `"global"`. + +- - - + + + +## Attributes Reference + +In addition to the arguments listed above, the following computed attributes are exported: + +* `id` - Identifier of the TagBindingCollection resource, in the format `locations/{location}/tagBindingCollections/{encoded_full_resource_name}` + +* `name` - + The name of the TagBindingCollection, in the format: `locations/{location}/tagBindingCollections/{encoded_full_resource_name}` + +* `active_tags` - + The most recent state of all direct tags on the resource, as reported by the API. + This includes the tags configured through Terraform, Google system tags, and tags attached by other clients. + + +## Timeouts + +This resource provides the following +[Timeouts](/docs/configuration/resources.html#timeouts) configuration options: + +- `create` - Default is 20 minutes. +- `update` - Default is 20 minutes. +- `delete` - Default is 20 minutes. + +## Import + +> **Note on Import and Non-Authoritative Behavior:** Because `tags` is a non-authoritative field, `terraform import` does not automatically populate the desired `tags` map in your configuration, to avoid inadvertently adopting system tags or unmanaged third-party tags. After running `terraform import`, inspect the computed `active_tags` attribute in your state file to discover all tags currently attached to the physical resource, and explicitly add only the specific keys you wish Terraform to manage into your `.tf` `tags` map. + +TagBindingCollection can be imported using any of these accepted formats: + +* `locations/{{location}}/tagBindingCollections/{{encoded_full_resource_name}}` + +In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import TagBindingCollection using one of the formats above. For example: + +```tf +import { + id = "locations/{{location}}/tagBindingCollections/{{encoded_full_resource_name}}" + to = google_tags_tag_binding_collection.default +} +``` + +When using the [`terraform import` command](https://developer.hashicorp.com/terraform/cli/commands/import), TagBindingCollection can be imported using one of the formats above. For example: + +``` +$ terraform import google_tags_tag_binding_collection.default locations/{{location}}/tagBindingCollections/{{encoded_full_resource_name}} +``` +{{- end }} From 8282a40f49c546e4f9b9d9382095ff4f1341d5e5 Mon Sep 17 00:00:00 2001 From: gurusai-voleti Date: Wed, 1 Jul 2026 05:14:09 +0530 Subject: [PATCH 19/78] storage: rapid storage example (#17964) --- .../website/docs/r/storage_bucket.html.markdown | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/mmv1/third_party/terraform/website/docs/r/storage_bucket.html.markdown b/mmv1/third_party/terraform/website/docs/r/storage_bucket.html.markdown index 5245e1e77203..042bc569216d 100644 --- a/mmv1/third_party/terraform/website/docs/r/storage_bucket.html.markdown +++ b/mmv1/third_party/terraform/website/docs/r/storage_bucket.html.markdown @@ -177,6 +177,21 @@ resource "google_storage_bucket" "hns-enabled" { } } ``` +## Example Usage - Enabling RAPID storage bucket + +```hcl +resource "google_storage_bucket" "zonal_bucket" { + location = "US-CENTRAL1" + custom_placement_config { + data_locations = ["US-CENTRAL1-B"] + } + name = "zonal-rapid-bucket" + storage_class = "RAPID" + hierarchical_namespace { + enabled = true + } +} +``` ## Argument Reference From 2e6e87a441844db3fc9cace2527561584ebf8cd3 Mon Sep 17 00:00:00 2001 From: rpgoog <266194224+rpgoog@users.noreply.github.com> Date: Tue, 30 Jun 2026 17:35:56 -0700 Subject: [PATCH 20/78] Fix Terraform timeout during cluster creation (#18143) --- mmv1/products/hypercomputecluster/Cluster.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mmv1/products/hypercomputecluster/Cluster.yaml b/mmv1/products/hypercomputecluster/Cluster.yaml index d19f60bf432b..e8373b0e65ca 100644 --- a/mmv1/products/hypercomputecluster/Cluster.yaml +++ b/mmv1/products/hypercomputecluster/Cluster.yaml @@ -21,6 +21,10 @@ update_mask: true update_verb: PATCH import_format: - projects/{{project}}/locations/{{location}}/clusters/{{cluster_id}} +timeouts: + insert_minutes: 60 + update_minutes: 60 + delete_minutes: 60 async: operation: timeouts: From 92556ab0cf7a5a97158b583a52ce199b40000967 Mon Sep 17 00:00:00 2001 From: Sam Levenick Date: Wed, 1 Jul 2026 09:31:09 -0700 Subject: [PATCH 21/78] Tag Schedule as autogen v2 (#18060) --- mmv1/products/vertexai/Schedule.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mmv1/products/vertexai/Schedule.yaml b/mmv1/products/vertexai/Schedule.yaml index cd219bd951b5..adf542905084 100644 --- a/mmv1/products/vertexai/Schedule.yaml +++ b/mmv1/products/vertexai/Schedule.yaml @@ -27,7 +27,7 @@ async: - delete result: resource_inside_response: true -autogen_status: U2NoZWR1bGU= +autogen_status: U2NoZWR1bGVBdXRvZ2VuVjJBZ2VudA== autogen_async: false samples: - name: vertex_ai_schedule From e2e6b5a14b6e3f59006ddb1f292ad58ddb1215fe Mon Sep 17 00:00:00 2001 From: Vaibhav Rau Date: Wed, 1 Jul 2026 11:47:50 -0500 Subject: [PATCH 22/78] refactor: extract FlattenProjectIamCustomRole from Read function (#18141) --- ...resource_google_project_iam_custom_role.go | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/mmv1/third_party/terraform/services/resourcemanager/resource_google_project_iam_custom_role.go b/mmv1/third_party/terraform/services/resourcemanager/resource_google_project_iam_custom_role.go index f6314b8a26c0..d9fead3f97fb 100644 --- a/mmv1/third_party/terraform/services/resourcemanager/resource_google_project_iam_custom_role.go +++ b/mmv1/third_party/terraform/services/resourcemanager/resource_google_project_iam_custom_role.go @@ -159,7 +159,21 @@ func resourceGoogleProjectIamCustomRoleRead(d *schema.ResourceData, meta interfa return transport_tpg.HandleNotFoundError(err, d, d.Id()) } - if err := d.Set("role_id", tpgresource.GetResourceNameFromSelfLink(role.Name)); err != nil { + if err := FlattenProjectIamCustomRole(d, role, project); err != nil { + return err + } + + if err := tpgresource.DeletionPolicyReadDefault(d, config, "DELETE"); err != nil { + return err + } + + return nil +} + +func FlattenProjectIamCustomRole(d *schema.ResourceData, role *iam.Role, project string) error { + roleID := tpgresource.GetResourceNameFromSelfLink(role.Name) + + if err := d.Set("role_id", roleID); err != nil { return fmt.Errorf("Error setting role_id: %s", err) } if err := d.Set("title", role.Title); err != nil { @@ -184,10 +198,6 @@ func resourceGoogleProjectIamCustomRoleRead(d *schema.ResourceData, meta interfa return fmt.Errorf("Error setting project: %s", err) } - if err := tpgresource.DeletionPolicyReadDefault(d, config, "DELETE"); err != nil { - return err - } - return nil } From 884a23a98fe4851dcd040ebc9ac6bf5df0b59755 Mon Sep 17 00:00:00 2001 From: Matt-Ryan <2506963+Matt-Ryan@users.noreply.github.com> Date: Wed, 1 Jul 2026 11:30:44 -0600 Subject: [PATCH 23/78] vertexai: add google_vertex_ai_semantic_governance_policy_engine (beta) (#18040) --- .../SemanticGovernancePolicyEngine.yaml | 127 ++++++++++++++++++ .../semantic_governance_policy_engine.go.tmpl | 37 +++++ ...tic_governance_policy_engine_basic.tf.tmpl | 4 + ..._semantic_governance_policy_engine.go.tmpl | 6 + ...rnance_policy_engine_internal_test.go.tmpl | 84 ++++++++++++ ...ntic_governance_policy_engine_test.go.tmpl | 120 +++++++++++++++++ .../issue-labeler/labeler/enrolled_teams.yml | 3 + 7 files changed, 381 insertions(+) create mode 100644 mmv1/products/vertexai/SemanticGovernancePolicyEngine.yaml create mode 100644 mmv1/templates/terraform/constants/semantic_governance_policy_engine.go.tmpl create mode 100644 mmv1/templates/terraform/examples/vertex_ai_semantic_governance_policy_engine_basic.tf.tmpl create mode 100644 mmv1/templates/terraform/post_read/vertex_ai_semantic_governance_policy_engine.go.tmpl create mode 100644 mmv1/third_party/terraform/services/vertexai/resource_vertex_ai_semantic_governance_policy_engine_internal_test.go.tmpl create mode 100644 mmv1/third_party/terraform/services/vertexai/resource_vertex_ai_semantic_governance_policy_engine_test.go.tmpl diff --git a/mmv1/products/vertexai/SemanticGovernancePolicyEngine.yaml b/mmv1/products/vertexai/SemanticGovernancePolicyEngine.yaml new file mode 100644 index 000000000000..5d48bcfe0af4 --- /dev/null +++ b/mmv1/products/vertexai/SemanticGovernancePolicyEngine.yaml @@ -0,0 +1,127 @@ +# 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: 'SemanticGovernancePolicyEngine' +description: | + A SemanticGovernancePolicyEngine (SGPE) is the managed, runtime evaluation + infrastructure for Semantic Governance Policies (SGP): the natural-language + constraints that govern an AI agent's tool calls. It is a project-level, + regional singleton, so each project has at most one engine per region. + + Provisioning the engine sets up managed Private Service Connect (PSC) + networking in your VPC and a policy decision point that the Agent Gateway + consults at runtime to allow or deny an agent's proposed tool calls. The + Semantic Governance Policies themselves, and the Agent Gateway integration + that routes agent traffic through the engine, are configured separately and + are not managed by this resource. + + Reading an uninitialized or deprovisioned engine returns the singleton + with state INACTIVE rather than reporting it as absent. +references: + guides: + 'Semantic governance overview': 'https://docs.cloud.google.com/gemini-enterprise-agent-platform/govern/policies/semantic-governance-overview' +docs: +min_version: 'beta' +id_format: 'projects/{{project}}/locations/{{region}}/semanticGovernancePolicyEngine' +base_url: 'projects/{{project}}/locations/{{region}}/semanticGovernancePolicyEngine' +self_link: 'projects/{{project}}/locations/{{region}}/semanticGovernancePolicyEngine' +create_url: 'projects/{{project}}/locations/{{region}}/semanticGovernancePolicyEngine' +# Singleton resource: AIP-156 prescribes upsert via PATCH with no parent +# Create RPC. +create_verb: 'PATCH' +update_url: 'projects/{{project}}/locations/{{region}}/semanticGovernancePolicyEngine' +update_verb: 'PATCH' +# No standard Delete; tear-down is the :deprovision custom method (POST, +# empty body), polled via the async block below. +delete_url: 'projects/{{project}}/locations/{{region}}/semanticGovernancePolicyEngine:deprovision' +delete_verb: 'POST' +import_format: + - 'projects/{{project}}/locations/{{region}}/semanticGovernancePolicyEngine' +timeouts: + insert_minutes: 60 + update_minutes: 60 + delete_minutes: 60 +async: + actions: ['create', 'update', 'delete'] + type: 'OpAsync' + operation: + base_url: '{{op_id}}' + result: + resource_inside_response: true +custom_code: + constants: 'templates/terraform/constants/semantic_governance_policy_engine.go.tmpl' + post_read: 'templates/terraform/post_read/vertex_ai_semantic_governance_policy_engine.go.tmpl' +examples: + - name: 'vertex_ai_semantic_governance_policy_engine_basic' + primary_resource_id: 'sgpe' + # The generator's standard CheckDestroy expects GET to return an error + # (404) after delete, but SGPE is an AIP-156 singleton whose GET returns + # 200 with state INACTIVE after deprovision -- so the example-derived + # test would fail at CheckDestroy even after a successful deprovision. + # Acceptance coverage is instead provided by the handwritten lifecycle + # test (resource_vertex_ai_semantic_governance_policy_engine_test.go), + # which CI still runs because it is a new TestAcc function in the diff. + exclude_test: true +parameters: + - name: 'region' + type: String + description: | + The region of the SemanticGovernancePolicyEngine, e.g. 'us-central1'. + url_param_only: true + immutable: true + default_from_api: true +properties: + - name: 'name' + type: String + description: | + The resource name of the SemanticGovernancePolicyEngine, in the form + 'projects/{project}/locations/{region}/semanticGovernancePolicyEngine'. + output: true + - name: 'createTime' + type: String + description: | + The time the SemanticGovernancePolicyEngine was created, in RFC3339 + UTC "Zulu" format. + output: true + - name: 'updateTime' + type: String + description: | + The time the SemanticGovernancePolicyEngine was last updated, in + RFC3339 UTC "Zulu" format. + output: true + - name: 'pscServiceAttachment' + type: String + description: | + The Private Service Connect service attachment URI for the SGPE's + managed endpoint. + output: true + - name: 'ipAddress' + type: String + description: | + The IP address allocated for the SGPE's managed PSC endpoint. + output: true + - name: 'pscForwardingRule' + type: String + description: | + The Private Service Connect forwarding rule URI for the SGPE's + managed endpoint. + output: true + - name: 'state' + type: String + description: | + The current state of the SemanticGovernancePolicyEngine. One of: + STATE_UNSPECIFIED, PROVISIONING, ACTIVE, FAILED, DEPROVISIONING, + INACTIVE. `FAILED` indicates provisioning did not succeed; recover by + destroying the resource (deprovision) or re-applying (re-provision). + output: true diff --git a/mmv1/templates/terraform/constants/semantic_governance_policy_engine.go.tmpl b/mmv1/templates/terraform/constants/semantic_governance_policy_engine.go.tmpl new file mode 100644 index 000000000000..31c0632547a2 --- /dev/null +++ b/mmv1/templates/terraform/constants/semantic_governance_policy_engine.go.tmpl @@ -0,0 +1,37 @@ +// Helper functions for the SemanticGovernancePolicyEngine (SGPE) resource, +// referenced from the resource YAML's custom_code.constants key. + +// sgpePostReadDispatch runs after the standard Read has fetched the engine +// into `res`, but before the schema is flattened from it (per resource.go.tmpl +// ordering). The API returns 200 + INACTIVE (not 404) for a deprovisioned or +// uninitialized singleton, so this translates INACTIVE into "gone" by clearing +// the ID; every other state is left in state. A missing or non-string `state` +// is a backend contract break and returns an error. +// +// It reads `res["state"]` directly because the schema flatten has not yet +// populated d.Get("state") at this point, and accepts the minimal +// sgpeResourceID interface rather than *schema.ResourceData so unit tests can +// substitute a fake. +type sgpeResourceID interface { + Id() string + SetId(string) +} + +func sgpePostReadDispatch(d sgpeResourceID, res map[string]interface{}) error { + state, ok := res["state"].(string) + if !ok { + return fmt.Errorf("SGPE response is missing or has malformed state field; backend regression.") + } + // Only INACTIVE clears the ID (treat as deleted; next apply re-creates). + // Every other state — including FAILED and any unrecognized state a newer + // backend may add — is intentionally left in state. FAILED is a real + // terminal state (per the SGPE API a FAILED engine may be re-provisioned + // or deprovisioned), so we surface it via the Computed `state` attribute + // and let the user choose to destroy (deprovision) or re-apply + // (re-provision). + if state == "INACTIVE" { + log.Printf("[WARN] SemanticGovernancePolicyEngine %q is INACTIVE; treating as deleted, next apply will re-create", d.Id()) + d.SetId("") + } + return nil +} diff --git a/mmv1/templates/terraform/examples/vertex_ai_semantic_governance_policy_engine_basic.tf.tmpl b/mmv1/templates/terraform/examples/vertex_ai_semantic_governance_policy_engine_basic.tf.tmpl new file mode 100644 index 000000000000..42fb9d607991 --- /dev/null +++ b/mmv1/templates/terraform/examples/vertex_ai_semantic_governance_policy_engine_basic.tf.tmpl @@ -0,0 +1,4 @@ +resource "google_vertex_ai_semantic_governance_policy_engine" "{{$.PrimaryResourceId}}" { + region = "us-central1" + provider = google-beta +} diff --git a/mmv1/templates/terraform/post_read/vertex_ai_semantic_governance_policy_engine.go.tmpl b/mmv1/templates/terraform/post_read/vertex_ai_semantic_governance_policy_engine.go.tmpl new file mode 100644 index 000000000000..1385565c7b4c --- /dev/null +++ b/mmv1/templates/terraform/post_read/vertex_ai_semantic_governance_policy_engine.go.tmpl @@ -0,0 +1,6 @@ +// SemanticGovernancePolicyEngine post-Read state dispatch. Logic lives in +// sgpePostReadDispatch (constants template) so it can be unit-tested directly +// via the sgpeResourceID interface (see the resource's internal test). +if err := sgpePostReadDispatch(d, res); err != nil { + return err +} diff --git a/mmv1/third_party/terraform/services/vertexai/resource_vertex_ai_semantic_governance_policy_engine_internal_test.go.tmpl b/mmv1/third_party/terraform/services/vertexai/resource_vertex_ai_semantic_governance_policy_engine_internal_test.go.tmpl new file mode 100644 index 000000000000..e54d09701e2a --- /dev/null +++ b/mmv1/third_party/terraform/services/vertexai/resource_vertex_ai_semantic_governance_policy_engine_internal_test.go.tmpl @@ -0,0 +1,84 @@ +{{- if ne $.TargetVersionName "ga" -}} +package vertexai + +import ( + "testing" +) + +// Unit tests for the SemanticGovernancePolicyEngine helpers declared in +// mmv1/templates/terraform/constants/semantic_governance_policy_engine.go.tmpl +// (generated into resource_vertex_ai_semantic_governance_policy_engine.go). + +// fakeResourceData implements sgpeResourceID and records every SetId call, +// so tests can assert whether (and how often) post_read clears the ID. +type fakeResourceData struct { + id string + setIdArgs []string +} + +func (f *fakeResourceData) Id() string { return f.id } +func (f *fakeResourceData) SetId(v string) { f.setIdArgs = append(f.setIdArgs, v); f.id = v } + +// post_read translates INACTIVE to SetId("") so the next plan re-creates. +func TestUnitSGPE_PostReadDispatch_INACTIVE_clears_id(t *testing.T) { + t.Parallel() + d := &fakeResourceData{id: "projects/p/locations/us-central1/semanticGovernancePolicyEngine"} + res := map[string]interface{}{"state": "INACTIVE"} + if err := sgpePostReadDispatch(d, res); err != nil { + t.Fatalf("expected nil error on INACTIVE, got %v", err) + } + if d.Id() != "" { + t.Errorf("expected SetId(\"\") to clear the ID; got %q", d.Id()) + } + if len(d.setIdArgs) != 1 || d.setIdArgs[0] != "" { + t.Errorf("expected exactly one SetId(\"\") call; got %v", d.setIdArgs) + } +} + +// post_read returns an error only when the state field is missing or not a +// string (a backend contract break). Unrecognized enum values are left +// tracked, not errored — see TestUnitSGPE_PostReadDispatch_inStateBranches_no_op. +func TestUnitSGPE_PostReadDispatch_malformedState_return_error(t *testing.T) { + t.Parallel() + cases := map[string]map[string]interface{}{ + "state field missing": {}, + "state field is not a string": {"state": 42}, + } + for name, res := range cases { + t.Run(name, func(t *testing.T) { + d := &fakeResourceData{id: "irrelevant"} + err := sgpePostReadDispatch(d, res) + if err == nil { + t.Fatalf("expected non-nil error for case %q; got nil", name) + } + if d.Id() == "" { + t.Errorf("error path must NOT clear the ID; SetId(\"\") was called for case %q", name) + } + }) + } +} + +// The no-op branch of the post_read contract: for every non-INACTIVE +// state the ID is left intact, so Terraform keeps the resource and does +// not plan a re-create. (Only INACTIVE clears the ID — see the test above.) +func TestUnitSGPE_PostReadDispatch_inStateBranches_no_op(t *testing.T) { + t.Parallel() + for _, state := range []string{"ACTIVE", "FAILED", "PROVISIONING", "DEPROVISIONING", "STATE_UNSPECIFIED", "FUTURE_VALUE_NOT_YET_SUPPORTED"} { + t.Run(state, func(t *testing.T) { + origID := "projects/p/locations/us-central1/semanticGovernancePolicyEngine" + d := &fakeResourceData{id: origID} + err := sgpePostReadDispatch(d, map[string]interface{}{"state": state}) + if err != nil { + t.Fatalf("expected nil error for state %q; got %v", state, err) + } + if d.Id() != origID { + t.Errorf("state %q must NOT clear the ID; ID is now %q", state, d.Id()) + } + if len(d.setIdArgs) != 0 { + t.Errorf("state %q must NOT call SetId; got SetId calls %v", state, d.setIdArgs) + } + }) + } +} + +{{- end }} diff --git a/mmv1/third_party/terraform/services/vertexai/resource_vertex_ai_semantic_governance_policy_engine_test.go.tmpl b/mmv1/third_party/terraform/services/vertexai/resource_vertex_ai_semantic_governance_policy_engine_test.go.tmpl new file mode 100644 index 000000000000..9819d6314a73 --- /dev/null +++ b/mmv1/third_party/terraform/services/vertexai/resource_vertex_ai_semantic_governance_policy_engine_test.go.tmpl @@ -0,0 +1,120 @@ +package vertexai_test +{{- if ne $.TargetVersionName "ga" }} + +import ( + "fmt" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + + "github.com/hashicorp/terraform-provider-google/google/acctest" + "github.com/hashicorp/terraform-provider-google/google/registry" + "github.com/hashicorp/terraform-provider-google/google/tpgresource" + transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" +) + +// Handwritten lifecycle test (Create + Import; deprovision via CheckDestroy). +// The generated example is exclude_test — see the singleton-aware +// CheckDestroy below for why. +func TestAccVertexAISemanticGovernancePolicyEngine_lifecycle(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{} + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderBetaFactories(t), + CheckDestroy: testAccCheckVertexAISemanticGovernancePolicyEngineDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccVertexAISemanticGovernancePolicyEngine_basic(context), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "google_vertex_ai_semantic_governance_policy_engine.sgpe", + "state", "ACTIVE"), + resource.TestCheckResourceAttrSet( + "google_vertex_ai_semantic_governance_policy_engine.sgpe", + "name"), + resource.TestCheckResourceAttrSet( + "google_vertex_ai_semantic_governance_policy_engine.sgpe", + "create_time"), + resource.TestCheckResourceAttrSet( + "google_vertex_ai_semantic_governance_policy_engine.sgpe", + "psc_service_attachment"), + // ip_address and psc_forwarding_rule are not asserted: they + // populate only after a consumer creates a PSC forwarding + // rule, which the basic fixture has none of. + ), + }, + { + ResourceName: "google_vertex_ai_semantic_governance_policy_engine.sgpe", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccVertexAISemanticGovernancePolicyEngine_basic(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_vertex_ai_semantic_governance_policy_engine" "sgpe" { + provider = google-beta + region = "us-central1" +} +`, context) +} + +// testAccCheckVertexAISemanticGovernancePolicyEngineDestroyProducer +// asserts the engine has been deprovisioned. SGPE is a singleton per +// AIP-156: a Get against a deprovisioned engine returns HTTP 200 with +// state INACTIVE, NOT a 404. This destroy check therefore asserts +// state == "INACTIVE", not "GET returns error". +func testAccCheckVertexAISemanticGovernancePolicyEngineDestroyProducer(t *testing.T) func(s *terraform.State) error { + return func(s *terraform.State) error { + for name, rs := range s.RootModule().Resources { + if rs.Type != "google_vertex_ai_semantic_governance_policy_engine" { + continue + } + if strings.HasPrefix(name, "data.") { + continue + } + + config := acctest.GoogleProviderConfig(t) + + url, err := tpgresource.ReplaceVarsForTest(config, rs, transport_tpg.BaseUrl(registry.GetProduct("vertexai"), config)+"projects/{{"{{"}}project{{"}}"}}/locations/{{"{{"}}region{{"}}"}}/semanticGovernancePolicyEngine") + if err != nil { + return err + } + + billingProject := "" + if config.BillingProject != "" { + billingProject = config.BillingProject + } + + res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "GET", + Project: billingProject, + RawURL: url, + UserAgent: config.UserAgent, + }) + if err != nil { + // Unexpected error (not the "200 with INACTIVE" we expect). + return fmt.Errorf("unexpected error checking SemanticGovernancePolicyEngine deprovision state at %s: %s", url, err) + } + + state, ok := res["state"].(string) + if !ok { + return fmt.Errorf("SemanticGovernancePolicyEngine GET at %s returned no state field; backend regression", url) + } + if state != "INACTIVE" { + return fmt.Errorf("SemanticGovernancePolicyEngine at %s was not deprovisioned: state is %q (expected INACTIVE)", url, state) + } + } + return nil + } +} + +{{ end }} diff --git a/tools/issue-labeler/labeler/enrolled_teams.yml b/tools/issue-labeler/labeler/enrolled_teams.yml index 3718120f54a7..d7cca74aa659 100755 --- a/tools/issue-labeler/labeler/enrolled_teams.yml +++ b/tools/issue-labeler/labeler/enrolled_teams.yml @@ -37,6 +37,9 @@ service/aiplatform-prediction: service/aiplatform-rag-engine: resources: - google_vertex_ai_rag_engine_config +service/aiplatform-semantic-governance: + resources: + - google_vertex_ai_semantic_governance.* service/aiplatform-tensorboard: resources: - google_vertex_ai_tensorboard From 3fe46fd53250bd43eb2e7446fe2c2ba5f1512a36 Mon Sep 17 00:00:00 2001 From: xuchenma <67921399+xuchenma@users.noreply.github.com> Date: Wed, 1 Jul 2026 14:03:56 -0700 Subject: [PATCH 24/78] apigee: add regression test for google_apigee_organization billing_type (#17946) Signed-off-by: Xuchen Ma --- ...e_apigee_organization_billing_type_test.go | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 mmv1/third_party/terraform/services/apigee/resource_apigee_organization_billing_type_test.go diff --git a/mmv1/third_party/terraform/services/apigee/resource_apigee_organization_billing_type_test.go b/mmv1/third_party/terraform/services/apigee/resource_apigee_organization_billing_type_test.go new file mode 100644 index 000000000000..8ef4e5ee05dd --- /dev/null +++ b/mmv1/third_party/terraform/services/apigee/resource_apigee_organization_billing_type_test.go @@ -0,0 +1,92 @@ +package apigee_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + + "github.com/hashicorp/terraform-provider-google/google/acctest" + "github.com/hashicorp/terraform-provider-google/google/envvar" + _ "github.com/hashicorp/terraform-provider-google/google/services/apigee" + _ "github.com/hashicorp/terraform-provider-google/google/services/resourcemanager" +) + +// TestAccApigeeOrganization_apigeeOrganizationBillingType verifies that an +// explicitly-specified billing_type is sent on create and honored by the API +// (regression test for hashicorp/terraform-provider-google#17731, where +// billing_type was omitted from the create request and the org came back with +// the default billing type instead of the requested one). +func TestAccApigeeOrganization_apigeeOrganizationBillingType(t *testing.T) { + acctest.SkipIfVcr(t) + t.Parallel() + + context := map[string]interface{}{ + "billing_account": envvar.GetTestBillingAccountFromEnv(t), + "org_id": envvar.GetTestOrgFromEnv(t), + "random_suffix": acctest.RandString(t, 10), + } + + resourceName := "google_apigee_organization.apigee_org" + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + ExternalProviders: map[string]resource.ExternalProvider{ + "time": {}, + }, + CheckDestroy: testAccCheckApigeeOrganizationDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccApigeeOrganization_billingType(context), + Check: resource.ComposeTestCheckFunc( + // The requested non-default billing type must round-trip. + resource.TestCheckResourceAttr(resourceName, "billing_type", "PAYG"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"retention"}, + }, + }, + }) +} + +func testAccApigeeOrganization_billingType(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_project" "project" { + project_id = "tf-test%{random_suffix}" + name = "tf-test%{random_suffix}" + org_id = "%{org_id}" + billing_account = "%{billing_account}" + deletion_policy = "DELETE" +} + +resource "google_project_service" "apigee" { + project = google_project.project.project_id + service = "apigee.googleapis.com" +} + +resource "time_sleep" "wait_300_seconds" { + create_duration = "300s" + depends_on = [google_project_service.apigee] +} + +resource "time_sleep" "wait_after_destroy" { + destroy_duration = "150s" +} + +resource "google_apigee_organization" "apigee_org" { + description = "Terraform-provisioned Apigee Org with an explicit billing type." + analytics_region = "us-central1" + project_id = google_project.project.project_id + disable_vpc_peering = true + billing_type = "PAYG" + depends_on = [ + time_sleep.wait_300_seconds, + time_sleep.wait_after_destroy, + ] +} +`, context) +} From d3b85a12172e46bb954c94ac14cce9f352089f91 Mon Sep 17 00:00:00 2001 From: virgulati2003 <108871727+virgulati2003@users.noreply.github.com> Date: Thu, 2 Jul 2026 02:55:37 +0530 Subject: [PATCH 25/78] =?UTF-8?q?Add=20exascaleDbStorageVault=20and=20stor?= =?UTF-8?q?ageManagementType=20fields=20to=20CloudV=E2=80=A6=20(#18145)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../oracledatabase/CloudVmCluster.yaml | 35 ++++++++ ...edatabase_cloud_vmcluster_exascale.tf.tmpl | 83 +++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 mmv1/templates/terraform/samples/services/oracledatabase/oracledatabase_cloud_vmcluster_exascale.tf.tmpl diff --git a/mmv1/products/oracledatabase/CloudVmCluster.yaml b/mmv1/products/oracledatabase/CloudVmCluster.yaml index c90dafe6d2c1..c2f28f00b215 100644 --- a/mmv1/products/oracledatabase/CloudVmCluster.yaml +++ b/mmv1/products/oracledatabase/CloudVmCluster.yaml @@ -113,6 +113,24 @@ samples: # See: https://github.com/hashicorp/terraform-provider-google/issues/20599 cloud_vm_cluster_id: fmt.Sprintf("ofake-tf-test-vmcluster-full-%s", acctest.RandString(t, 10)) cloud_exadata_infrastructure_id: fmt.Sprintf("ofake-tf-test-exadata-for-vmcluster-full-%s", acctest.RandString(t, 10)) + - name: oracledatabase_cloud_vmcluster_exascale + primary_resource_id: my_vmcluster + steps: + - name: oracledatabase_cloud_vmcluster_exascale + resource_id_vars: + project: my-project + cloud_vm_cluster_id: my-instance + cloud_exadata_infrastructure_id: my-exadata + exascale_db_storage_vault_id: my-vault + deletion_protection: "true" + ignore_read_extra: + - deletion_protection + test_vars_overrides: + deletion_protection: "false" + project: '"oci-terraform-testing-prod"' + cloud_vm_cluster_id: fmt.Sprintf("ofake-tf-test-vmcluster-exascale-%s", acctest.RandString(t, 10)) + cloud_exadata_infrastructure_id: fmt.Sprintf("ofake-tf-test-exadata-for-vmcluster-exascale-%s", acctest.RandString(t, 10)) + exascale_db_storage_vault_id: fmt.Sprintf("ofake-tf-test-vault-for-vmcluster-exascale-%s", acctest.RandString(t, 10)) virtual_fields: - name: deletion_protection type: Boolean @@ -300,6 +318,15 @@ properties: type: String description: 'OCI Cluster name. ' default_from_api: true + - name: storageManagementType + type: String + description: |- + The storage management type of the VM Cluster. + Possible values: + STORAGE_MANAGEMENT_TYPE_UNSPECIFIED + ASM + EXASCALE + output: true - name: labels type: KeyValueLabels description: 'Labels or tags associated with the VM Cluster. ' @@ -364,3 +391,11 @@ properties: PARTIALLY_CONNECTED DISCONNECTED UNKNOWN + - name: exascaleDbStorageVault + type: String + description: |- + The name of ExascaleDbStorageVault associated with the VM Cluster. + Format: + projects/{project}/locations/{location}/exascaleDbStorageVaults/{exascale_db_storage_vault} + immutable: true + diff_suppress_func: tpgresource.CompareSelfLinkOrResourceName diff --git a/mmv1/templates/terraform/samples/services/oracledatabase/oracledatabase_cloud_vmcluster_exascale.tf.tmpl b/mmv1/templates/terraform/samples/services/oracledatabase/oracledatabase_cloud_vmcluster_exascale.tf.tmpl new file mode 100644 index 000000000000..da5d5c945fc1 --- /dev/null +++ b/mmv1/templates/terraform/samples/services/oracledatabase/oracledatabase_cloud_vmcluster_exascale.tf.tmpl @@ -0,0 +1,83 @@ +resource "google_oracle_database_cloud_vm_cluster" "{{$.PrimaryResourceId}}" { + cloud_vm_cluster_id = "{{index $.ResourceIdVars "cloud_vm_cluster_id"}}" + display_name = "{{index $.ResourceIdVars "cloud_vm_cluster_id"}} displayname" + location = "us-east4" + project = "{{index $.ResourceIdVars "project"}}" + exadata_infrastructure = google_oracle_database_cloud_exadata_infrastructure.infra.id + network = data.google_compute_network.default.id + cidr = "10.5.0.0/24" + backup_subnet_cidr = "10.6.0.0/24" + + exascale_db_storage_vault = google_oracle_database_exascale_db_storage_vault.vault.name + + properties { + license_type = "LICENSE_INCLUDED" + ssh_public_keys = ["ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCz1X2744t+6vRLmE5u6nHi6/QWh8bQDgHmd+OIxRQIGA/IWUtCs2FnaCNZcqvZkaeyjk5v0lTA/n+9jvO42Ipib53athrfVG8gRt8fzPL66C6ZqHq+6zZophhrCdfJh/0G4x9xJh5gdMprlaCR1P8yAaVvhBQSKGc4SiIkyMNBcHJ5YTtMQMTfxaB4G1sHZ6SDAY9a6Cq/zNjDwfPapWLsiP4mRhE5SSjJX6l6EYbkm0JeLQg+AbJiNEPvrvDp1wtTxzlPJtIivthmLMThFxK7+DkrYFuLvN5AHUdo9KTDLvHtDCvV70r8v0gafsrKkM/OE9Jtzoo0e1N/5K/ZdyFRbAkFT4QSF3nwpbmBWLf2Evg//YyEuxnz4CwPqFST2mucnrCCGCVWp1vnHZ0y30nM35njLOmWdRDFy5l27pKUTwLp02y3UYiiZyP7d3/u5pKiN4vC27VuvzprSdJxWoAvluOiDeRh+/oeQDowxoT/Oop8DzB9uJmjktXw8jyMW2+Rpg+ENQqeNgF1OGlEzypaWiRskEFlkpLb4v/s3ZDYkL1oW0Nv/J8LTjTOTEaYt2Udjoe9x2xWiGnQixhdChWuG+MaoWffzUgx1tsVj/DBXijR5DjkPkrA1GA98zd3q8GKEaAdcDenJjHhNYSd4+rE9pIsnYn7fo5X/tFfcQH1XQ== nobody@google.com"] + cpu_core_count = "4" + gi_version = "23.0.0.0" + hostname_prefix = "hostname1" + + # Required fields for Exascale-based VM Clusters: + memory_size_gb = 60 + db_node_storage_size_gb = 120 + db_server_ocids = [ + data.google_oracle_database_db_servers.db_servers.db_servers.0.properties.0.ocid, + data.google_oracle_database_db_servers.db_servers.db_servers.1.properties.0.ocid + ] + } + + deletion_protection = "{{index $.ResourceIdVars "deletion_protection"}}" + + depends_on = [google_oracle_database_cloud_exadata_infrastructure_exascale_config.exascale_config] +} + +resource "google_oracle_database_cloud_exadata_infrastructure" "infra" { + cloud_exadata_infrastructure_id = "{{index $.ResourceIdVars "cloud_exadata_infrastructure_id"}}" + display_name = "{{index $.ResourceIdVars "cloud_exadata_infrastructure_id"}} displayname" + location = "us-east4" + project = "{{index $.ResourceIdVars "project"}}" + properties { + shape = "Exadata.X9M" + compute_count = "2" + storage_count = "3" + } + + deletion_protection = "{{index $.ResourceIdVars "deletion_protection"}}" +} + +resource "google_oracle_database_cloud_exadata_infrastructure_exascale_config" "exascale_config" { + cloud_exadata_infrastructure = google_oracle_database_cloud_exadata_infrastructure.infra.cloud_exadata_infrastructure_id + location = "us-east4" + project = "{{index $.ResourceIdVars "project"}}" + total_storage_size_gb = 10240 +} + +resource "google_oracle_database_exascale_db_storage_vault" "vault" { + exascale_db_storage_vault_id = "{{index $.ResourceIdVars "exascale_db_storage_vault_id"}}" + display_name = "{{index $.ResourceIdVars "exascale_db_storage_vault_id"}} displayname" + location = "us-east4" + project = "{{index $.ResourceIdVars "project"}}" + + exadata_infrastructure = google_oracle_database_cloud_exadata_infrastructure.infra.name + + depends_on = [google_oracle_database_cloud_exadata_infrastructure_exascale_config.exascale_config] + + properties { + exascale_db_storage_details { + total_size_gbs = 2048 + } + } + + deletion_protection = "{{index $.ResourceIdVars "deletion_protection"}}" +} + +data "google_compute_network" "default" { + name = "new" + project = "{{index $.ResourceIdVars "project"}}" +} + +data "google_oracle_database_db_servers" "db_servers" { + location = "us-east4" + project = "{{index $.ResourceIdVars "project"}}" + cloud_exadata_infrastructure = google_oracle_database_cloud_exadata_infrastructure.infra.cloud_exadata_infrastructure_id +} From 4f449c1377d863d21b97fc6dd9254641d4d8edac Mon Sep 17 00:00:00 2001 From: Enden Date: Thu, 2 Jul 2026 00:55:02 +0300 Subject: [PATCH 26/78] Add google_chronicle_environmentgroup resource to chronicle (#17712) Co-authored-by: Eden Shalit --- mmv1/products/chronicle/EnvironmentGroup.yaml | 89 +++++++++++++++++++ .../chronicle_environmentgroup_basic.tf.tmpl | 11 +++ .../chronicle_environmentgroup_full.tf.tmpl | 11 +++ 3 files changed, 111 insertions(+) create mode 100644 mmv1/products/chronicle/EnvironmentGroup.yaml create mode 100644 mmv1/templates/terraform/samples/services/chronicle/chronicle_environmentgroup_basic.tf.tmpl create mode 100644 mmv1/templates/terraform/samples/services/chronicle/chronicle_environmentgroup_full.tf.tmpl diff --git a/mmv1/products/chronicle/EnvironmentGroup.yaml b/mmv1/products/chronicle/EnvironmentGroup.yaml new file mode 100644 index 000000000000..f206ff2ef73a --- /dev/null +++ b/mmv1/products/chronicle/EnvironmentGroup.yaml @@ -0,0 +1,89 @@ +# 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: EnvironmentGroup +description: > + Environment groups let you organize multiple environments into logical categories, making it easier to manage large organizations or multiple customers as a Managed Security Service Provider (MSSP). +min_version: 'beta' +references: + guides: + 'Google SecOps Guides': 'https://cloud.google.com/chronicle/docs/secops/secops-overview' + api: 'https://docs.cloud.google.com/chronicle/docs/reference/rest/v1beta/projects.locations.instances.environmentGroups' +base_url: projects/{{project}}/locations/{{location}}/instances/{{instance}}/environmentGroups +self_link: projects/{{project}}/locations/{{location}}/instances/{{instance}}/environmentGroups/{{environment_group_id}} +create_url: projects/{{project}}/locations/{{location}}/instances/{{instance}}/environmentGroups +id_format: projects/{{project}}/locations/{{location}}/instances/{{instance}}/environmentGroups/{{environment_group_id}} +update_mask: true +update_verb: PATCH +import_format: + - projects/{{project}}/locations/{{location}}/instances/{{instance}}/environmentGroups/{{environment_group_id}} + +autogen_status: RW52aXJvbm1lbnRHcm91cA== + +samples: + - name: chronicle_environmentgroup_update + primary_resource_id: sample + min_version: beta + steps: + - name: "chronicle_environmentgroup_basic" + test_env_vars: + chronicle_id: 'CHRONICLE_ID' + - name: "chronicle_environmentgroup_full" + test_env_vars: + chronicle_id: 'CHRONICLE_ID' + +parameters: + - name: location + type: String + required: true + 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 + - name: instance + type: String + required: true + 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 + - name: environmentGroupId + type: String + output: true + custom_flatten: 'templates/terraform/custom_flatten/id_from_name.tmpl' + description: |- + Id of the environment group record. +properties: + - name: description + type: String + required: true + description: |- + The EnvironmentGroup description. + This value is optional. This value should be up to + 250 characters, and valid characters are /a-z-/. + - name: displayName + type: String + required: true + description: The group name. + - name: environmentsIds + type: Array + required: true + description: The environment IDs for the group. + item_type: + type: String + - name: name + type: String + description: |- + Identifier. The resource name of the environment group. + Format: + projects/{project}/locations/{location}/instances/{instance}/environmentGroups/{environment_group} + output: true diff --git a/mmv1/templates/terraform/samples/services/chronicle/chronicle_environmentgroup_basic.tf.tmpl b/mmv1/templates/terraform/samples/services/chronicle/chronicle_environmentgroup_basic.tf.tmpl new file mode 100644 index 000000000000..6dd8fb7354b6 --- /dev/null +++ b/mmv1/templates/terraform/samples/services/chronicle/chronicle_environmentgroup_basic.tf.tmpl @@ -0,0 +1,11 @@ +resource "google_chronicle_environment_group" "{{$.PrimaryResourceId}}" { + provider = google-beta + location = "us" + instance = "{{index $.TestEnvVars "chronicle_id"}}" + + display_name = "myEnvironmentGroup" + description = "My Environment Description" + environments_ids = [ + "Default Environment" + ] +} diff --git a/mmv1/templates/terraform/samples/services/chronicle/chronicle_environmentgroup_full.tf.tmpl b/mmv1/templates/terraform/samples/services/chronicle/chronicle_environmentgroup_full.tf.tmpl new file mode 100644 index 000000000000..52d15a61d7a7 --- /dev/null +++ b/mmv1/templates/terraform/samples/services/chronicle/chronicle_environmentgroup_full.tf.tmpl @@ -0,0 +1,11 @@ +resource "google_chronicle_environment_group" "{{$.PrimaryResourceId}}" { + provider = google-beta + location = "us" + instance = "{{index $.TestEnvVars "chronicle_id"}}" + + display_name = "myEnvironmentGroup1" + description = "My Environment Description1" + environments_ids = [ + "Default Environment" + ] +} From bce65454263a7bcca97cfecf1e13f85bdee5076c Mon Sep 17 00:00:00 2001 From: Enden Date: Mon, 6 Jul 2026 17:31:02 +0300 Subject: [PATCH 27/78] feat(chronicle): promote environment resource to GA (#18178) Co-authored-by: Eden Shalit --- mmv1/products/chronicle/Environment.yaml | 5 +---- .../services/chronicle/chronicle_environment_basic.tf.tmpl | 1 - .../services/chronicle/chronicle_environment_full.tf.tmpl | 1 - 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/mmv1/products/chronicle/Environment.yaml b/mmv1/products/chronicle/Environment.yaml index e6c5b30ad544..c9f2829f4c12 100644 --- a/mmv1/products/chronicle/Environment.yaml +++ b/mmv1/products/chronicle/Environment.yaml @@ -15,12 +15,10 @@ name: Environment description: > An environment is logical container for different networks or customers that are managed by the SOC or MSSP. This is useful for SOCs who provide services to several different networks, customers or business units within the organization. The Platform comes with a predefined environment named Default Environment. - -min_version: 'beta' references: guides: 'Google SecOps Guides': 'https://cloud.google.com/chronicle/docs/secops/secops-overview' - api: 'https://docs.cloud.google.com/chronicle/docs/reference/rest/v1beta/projects.locations.instances.environments' + api: 'https://docs.cloud.google.com/chronicle/docs/reference/rest/v1/projects.locations.instances.environments' base_url: projects/{{project}}/locations/{{location}}/instances/{{instance}}/environments self_link: projects/{{project}}/locations/{{location}}/instances/{{instance}}/environments/{{environment_id}} create_url: projects/{{project}}/locations/{{location}}/instances/{{instance}}/environments @@ -35,7 +33,6 @@ autogen_status: RW52aXJvbm1lbnQ= samples: - name: chronicle_environment_update primary_resource_id: sample - min_version: beta steps: - name: "chronicle_environment_basic" test_env_vars: diff --git a/mmv1/templates/terraform/samples/services/chronicle/chronicle_environment_basic.tf.tmpl b/mmv1/templates/terraform/samples/services/chronicle/chronicle_environment_basic.tf.tmpl index efc4481ad2bc..3644495e6d85 100644 --- a/mmv1/templates/terraform/samples/services/chronicle/chronicle_environment_basic.tf.tmpl +++ b/mmv1/templates/terraform/samples/services/chronicle/chronicle_environment_basic.tf.tmpl @@ -1,5 +1,4 @@ resource "google_chronicle_environment" "{{$.PrimaryResourceId}}" { - provider = google-beta location = "us" instance = "{{index $.TestEnvVars "chronicle_id"}}" diff --git a/mmv1/templates/terraform/samples/services/chronicle/chronicle_environment_full.tf.tmpl b/mmv1/templates/terraform/samples/services/chronicle/chronicle_environment_full.tf.tmpl index 01f21afc73ff..042f81df80b7 100644 --- a/mmv1/templates/terraform/samples/services/chronicle/chronicle_environment_full.tf.tmpl +++ b/mmv1/templates/terraform/samples/services/chronicle/chronicle_environment_full.tf.tmpl @@ -1,5 +1,4 @@ resource "google_chronicle_environment" "{{$.PrimaryResourceId}}" { - provider = google-beta location = "us" instance = "{{index $.TestEnvVars "chronicle_id"}}" From baad1c797d54cab281e8c777aa14e2a282dbe03e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2026 08:57:48 -0700 Subject: [PATCH 28/78] build(deps): bump golang.org/x/net from 0.48.0 to 0.55.0 in /.ci/magician (#18168) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .ci/magician/go.mod | 10 +++++----- .ci/magician/go.sum | 20 ++++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.ci/magician/go.mod b/.ci/magician/go.mod index 508be6d432a2..0a44acc810e6 100644 --- a/.ci/magician/go.mod +++ b/.ci/magician/go.mod @@ -66,12 +66,12 @@ require ( go.opentelemetry.io/otel/sdk v1.40.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect go.opentelemetry.io/otel/trace v1.41.0 // indirect - golang.org/x/crypto v0.46.0 // indirect - golang.org/x/net v0.48.0 // indirect + golang.org/x/crypto v0.51.0 // indirect + golang.org/x/net v0.55.0 // indirect golang.org/x/oauth2 v0.34.0 // indirect - golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.40.0 // indirect - golang.org/x/text v0.32.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.45.0 // indirect + golang.org/x/text v0.37.0 // indirect golang.org/x/time v0.8.0 // indirect google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect diff --git a/.ci/magician/go.sum b/.ci/magician/go.sum index 6940dd6bdac0..71f70ab7a77d 100644 --- a/.ci/magician/go.sum +++ b/.ci/magician/go.sum @@ -170,8 +170,8 @@ go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= -golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= +golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20230810033253-352e893a4cad h1:g0bG7Z4uG+OgH2QDODnjp6ggkk1bJDsINcuWmJN1iJU= golang.org/x/exp v0.0.0-20230810033253-352e893a4cad/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= @@ -184,26 +184,26 @@ golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= From 4137b52fa83d65843d57008da6353a91f7fd4475 Mon Sep 17 00:00:00 2001 From: Anhelina Belavezha <113670521+belovezhalin@users.noreply.github.com> Date: Mon, 6 Jul 2026 18:03:39 +0200 Subject: [PATCH 29/78] agentregistry: Resolve computed name field GA launch blockers (#18157) --- mmv1/products/agentregistry/Binding.yaml | 5 +++++ mmv1/products/agentregistry/Service.yaml | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/mmv1/products/agentregistry/Binding.yaml b/mmv1/products/agentregistry/Binding.yaml index 61ad43356a0a..5c6b587787d1 100644 --- a/mmv1/products/agentregistry/Binding.yaml +++ b/mmv1/products/agentregistry/Binding.yaml @@ -58,6 +58,11 @@ parameters: required: true url_param_only: true properties: + - name: name + type: String + description: | + The resource name of the Binding. + output: true - name: displayName type: String description: | diff --git a/mmv1/products/agentregistry/Service.yaml b/mmv1/products/agentregistry/Service.yaml index 019082232dc8..091f1764f5bb 100644 --- a/mmv1/products/agentregistry/Service.yaml +++ b/mmv1/products/agentregistry/Service.yaml @@ -61,6 +61,11 @@ parameters: required: true url_param_only: true properties: + - name: name + type: String + description: | + The resource name of the Service. + output: true - name: displayName type: String description: | From b7a1a2483a8802cdb250d8aac67d222be462a2cf Mon Sep 17 00:00:00 2001 From: Riley Karson Date: Mon, 6 Jul 2026 09:32:39 -0700 Subject: [PATCH 30/78] Update review vacations (#18183) --- .ci/magician/github/membership_data.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.ci/magician/github/membership_data.go b/.ci/magician/github/membership_data.go index c147312c10d5..f49fea6cb403 100644 --- a/.ci/magician/github/membership_data.go +++ b/.ci/magician/github/membership_data.go @@ -130,13 +130,17 @@ var ( startDate: newDate(2026, 5, 7), endDate: newDate(2026, 5, 11), }, + { + startDate: newDate(2026, 7, 5), + endDate: newDate(2026, 8, 17), + }, }, }, "ScottSuarez": { vacations: []Vacation{ { startDate: newDate(2026, 4, 4), - endDate: newDate(2026, 7, 13), + endDate: newDate(2026, 7, 5), }, }, }, From 7878f772e3b82182b1481161b4b8027cf667abad Mon Sep 17 00:00:00 2001 From: "Stephen Lewis (Burrows)" Date: Mon, 6 Jul 2026 10:28:27 -0700 Subject: [PATCH 31/78] Diff processor skill (#18107) --- .../skills/utils/check-schema-diff/SKILL.md | 42 +++ .../scripts/check_schema_diff.sh | 347 ++++++++++++++++++ .gitignore | 1 + 3 files changed, 390 insertions(+) create mode 100644 .agents/skills/utils/check-schema-diff/SKILL.md create mode 100755 .agents/skills/utils/check-schema-diff/scripts/check_schema_diff.sh diff --git a/.agents/skills/utils/check-schema-diff/SKILL.md b/.agents/skills/utils/check-schema-diff/SKILL.md new file mode 100644 index 000000000000..5e44a355f4f5 --- /dev/null +++ b/.agents/skills/utils/check-schema-diff/SKILL.md @@ -0,0 +1,42 @@ +--- +name: check-schema-diff +description: "Run comprehensive checks (breaking changes, missing tests, and missing documentation) on Magic Modules schema changes between the current branch and a base branch (usually main) using the local diff-processor." +--- + +# `check-schema-diff` + +> **Note to AI Agents:** You MUST read the YAML frontmatter above first. Only read the rest of this file if the `description` matches your required task. +> This skill allows you to run local breaking changes, missing tests, and missing documentation checks on Magic Modules schema changes. + +## Prerequisites + +- You must be operating in the `magic-modules` root directory. +- You must have `go` installed and configured. + +## Execution Steps + +### 1. Make the script executable (if not already) +Ensure the script is executable: +```bash +chmod +x .agents/skills/utils/check-schema-diff/scripts/check_schema_diff.sh +``` + +### 2. Run the Schema Diff Checker +Run the script to compare your current local changes (committed or uncommitted) against the base branch (defaults to the merge base with `origin/main` or `main`): +```bash +./.agents/skills/utils/check-schema-diff/scripts/check_schema_diff.sh +``` + +To compare against a specific branch or commit, pass it as an argument: +```bash +./.agents/skills/utils/check-schema-diff/scripts/check_schema_diff.sh +``` + +### 3. Analyze the Output +The tool runs three checks across both GA and Beta provider versions: +- **Breaking Changes:** Detects backwards-incompatible schema changes. +- **Missing Tests:** Identifies new or changed fields that are not covered by any acceptance tests. +- **Missing Documentation:** Identifies new fields that are not documented in the resource/datasource markdown files. + +### 4. Verification & Handoff +If any issues (breaking changes, missing tests, or missing docs) are detected, present them clearly to the user. Discuss potential mitigations and fixes before proceeding. diff --git a/.agents/skills/utils/check-schema-diff/scripts/check_schema_diff.sh b/.agents/skills/utils/check-schema-diff/scripts/check_schema_diff.sh new file mode 100755 index 000000000000..588b6ad1e63e --- /dev/null +++ b/.agents/skills/utils/check-schema-diff/scripts/check_schema_diff.sh @@ -0,0 +1,347 @@ +#!/usr/bin/env bash +# +# check_schema_diff.sh +# +# Automates local schema difference checks (breaking changes, missing tests, +# and missing documentation) for Magic Modules by comparing the current +# working tree against a base branch (e.g., main). +# +# Usage: +# ./.agents/skills/utils/check-schema-diff/scripts/check_schema_diff.sh [base_ref] +# + +set -e + +# Define color codes for pretty printing +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +echo -e "${BLUE}=== Magic Modules Schema Diff Checker ===${NC}" + +# 1. Validate environment +if [ ! -d "mmv1" ] || [ ! -d "tools/diff-processor" ]; then + echo -e "${RED}Error: This script must be run from the root of the magic-modules repository.${NC}" + exit 1 +fi + +# 2. Determine base ref +BASE_REF="$1" +if [ -z "$BASE_REF" ]; then + # Try to find the merge base with origin/main, fallback to main, fallback to origin/master + BASE_REF=$(git merge-base HEAD origin/main 2>/dev/null || git merge-base HEAD main 2>/dev/null || echo "main") +fi + +echo -e "${BLUE}Comparing current changes against base ref: ${YELLOW}${BASE_REF}${NC}" + +# 3. Set up paths +REPO_ROOT=$(pwd) +SCRATCH_DIR="${REPO_ROOT}/scratch/schema-diff-check" +PROVIDER_CACHE="${REPO_ROOT}/scratch/provider-cache" +DIFF_PROCESSOR_DIR="${REPO_ROOT}/tools/diff-processor" + +mkdir -p "$SCRATCH_DIR" + +# 4. Set up cleanup trap to ensure we never leave git worktrees behind +cleanup() { + echo -e "${BLUE}Cleaning up temporary files and worktrees...${NC}" + if [ -d "$SCRATCH_DIR/mm-base-worktree" ]; then + git worktree remove --force "$SCRATCH_DIR/mm-base-worktree" 2>/dev/null || true + fi +} +trap cleanup EXIT + +# Helper function to check if the docs JSON output has missing docs +has_missing_docs() { + local json="$1" + if command -v jq &> /dev/null; then + # Returns 0 (true) if there are missing docs, 1 (false) otherwise + echo "$json" | jq -e '(.Resource | length > 0) or (.DataSource | length > 0)' >/dev/null 2>&1 + return $? + else + # Simple Python fallback to check if either list is non-empty + python3 -c " +import sys, json +data = json.loads(sys.argv[1]) +resources = data.get('Resource') or [] +datasources = data.get('DataSource') or [] +if len(resources) > 0 or len(datasources) > 0: + sys.exit(0) +sys.exit(1) +" "$json" 2>/dev/null + return $? + fi +} + +# 5. Build current mmv1 binary in the current context (once for all checks) +echo -e "${BLUE}Building current mmv1 binary...${NC}" +if [ -f "MODULE.bazel" ] && command -v bazel &> /dev/null; then + bazel build //mmv1 + MM_BINARY="$(pwd)/bazel-bin/mmv1/mmv1_/mmv1" +else + ( + cd mmv1 + go build -o ../bin/mmv1 . + ) + MM_BINARY="$(pwd)/bin/mmv1" +fi + +# 6. Create temporary worktree for base commit (once for all checks) +echo -e "${BLUE}Creating temporary worktree for base commit ${YELLOW}${BASE_REF}${BLUE}...${NC}" +WORKTREE_DIR="$SCRATCH_DIR/mm-base-worktree" +rm -rf "$WORKTREE_DIR" +git worktree add --detach "$WORKTREE_DIR" "$BASE_REF" + + + +run_version_checks() { + local VERSION="$1" + local VERSION_UPPER=$(echo "$VERSION" | tr '[:lower:]' '[:upper:]') + local TARGET_DIFF_PROC="$SCRATCH_DIR/diff-proc-${VERSION}" + local PROVIDER_REPO + local PROVIDER_CACHE + local REAL_PACKAGE_NAME + local REAL_FOLDER_NAME + + if [ "$VERSION" = "ga" ]; then + PROVIDER_REPO="https://github.com/hashicorp/terraform-provider-google.git" + PROVIDER_CACHE="${REPO_ROOT}/scratch/provider-cache-ga" + REAL_PACKAGE_NAME="github.com/hashicorp/terraform-provider-google" + REAL_FOLDER_NAME="google" + else + PROVIDER_REPO="https://github.com/hashicorp/terraform-provider-google-beta.git" + if [ -d "${REPO_ROOT}/scratch/provider-cache" ] && [ ! -d "${REPO_ROOT}/scratch/provider-cache-beta" ]; then + PROVIDER_CACHE="${REPO_ROOT}/scratch/provider-cache" + else + PROVIDER_CACHE="${REPO_ROOT}/scratch/provider-cache-beta" + fi + REAL_PACKAGE_NAME="github.com/hashicorp/terraform-provider-google-beta" + REAL_FOLDER_NAME="google-beta" + fi + + # Prepare isolated diff-processor directory + rm -rf "$TARGET_DIFF_PROC" + mkdir -p "$TARGET_DIFF_PROC" + (cd "$DIFF_PROCESSOR_DIR" && tar cf - --exclude=old --exclude=new --exclude=bin .) | (cd "$TARGET_DIFF_PROC" && tar xf -) + + # Ensure we have a cached downstream provider clone + if [ ! -d "$PROVIDER_CACHE" ]; then + echo -e "${BLUE}[${VERSION_UPPER}] Cloning downstream provider into cache...${NC}" + git clone --depth 500 "$PROVIDER_REPO" "$PROVIDER_CACHE" >/dev/null 2>&1 + else + echo -e "${BLUE}[${VERSION_UPPER}] Updating cached downstream provider...${NC}" + ( + cd "$PROVIDER_CACHE" + git fetch --depth 500 origin main >/dev/null 2>&1 + git reset --hard origin/main >/dev/null 2>&1 + ) + fi + + # Find downstream commit matching base ref + echo -e "${BLUE}[${VERSION_UPPER}] Finding downstream commit matching base ref ${YELLOW}${BASE_REF}${BLUE}...${NC}" + local MATCHING_COMMIT="" + for MM_SHA in $(git log --format="%H" -n 100 "$BASE_REF"); do + MATCHING_COMMIT=$(cd "$PROVIDER_CACHE" && git log --grep="\[upstream:${MM_SHA}\]" --format="%H" -n 1) + if [ -n "$MATCHING_COMMIT" ]; then + echo -e "${GREEN}[${VERSION_UPPER}] Found matching provider commit: ${YELLOW}${MATCHING_COMMIT}${GREEN} (for MM commit ${MM_SHA:0:8})${NC}" + break + fi + done + + if [ -z "$MATCHING_COMMIT" ]; then + echo -e "${YELLOW}[${VERSION_UPPER}] Warning: No matching provider commit found in recent ancestors of ${BASE_REF}; using provider HEAD.${NC}" + MATCHING_COMMIT=$(cd "$PROVIDER_CACHE" && git rev-parse HEAD) + fi + + # Prepare old and new directories in diff-processor + echo -e "${BLUE}[${VERSION_UPPER}] Preparing diff-processor environment...${NC}" + cp -R "$PROVIDER_CACHE" "$TARGET_DIFF_PROC/old" + cp -R "$PROVIDER_CACHE" "$TARGET_DIFF_PROC/new" + (cd "$TARGET_DIFF_PROC/old" && git checkout "$MATCHING_COMMIT" >/dev/null 2>&1) + (cd "$TARGET_DIFF_PROC/new" && git checkout "$MATCHING_COMMIT" >/dev/null 2>&1) + + # Generate base and current provider code in parallel + echo -e "${BLUE}[${VERSION_UPPER}] Generating base and current provider code...${NC}" + ( + cd mmv1 + $MM_BINARY --output "$TARGET_DIFF_PROC/old" --version "$VERSION" --base "${WORKTREE_DIR}/mmv1" + ) & + local OLD_GEN_PID=$! + + ( + cd mmv1 + $MM_BINARY --output "$TARGET_DIFF_PROC/new" --version "$VERSION" + ) & + local NEW_GEN_PID=$! + + wait $OLD_GEN_PID + wait $NEW_GEN_PID + + # Perform package substitutions for side-by-side compilation + echo -e "${BLUE}[${VERSION_UPPER}] Preparing provider code for compilation...${NC}" + ( + cd "$TARGET_DIFF_PROC" + + # Old package substitution + ( + cd old/ + if [ -d "$REAL_FOLDER_NAME" ] && [ "$REAL_FOLDER_NAME" != "google" ]; then + mv "$REAL_FOLDER_NAME" google + fi + local fake_package_name=google/provider/old + + if [ "$(uname)" = "Darwin" ]; then + find . -type f -name "*.go" -exec sed -i "" "s~${REAL_PACKAGE_NAME}/${REAL_FOLDER_NAME}~${fake_package_name}/google~g" {} + + sed -i "" "s|${REAL_PACKAGE_NAME}|${fake_package_name}|g" go.mod + else + find . -type f -name "*.go" -exec sed -i "s~${REAL_PACKAGE_NAME}/${REAL_FOLDER_NAME}~${fake_package_name}/google~g" {} + + sed -i "s|${REAL_PACKAGE_NAME}|${fake_package_name}|g" go.mod + fi + ) & + local OLD_SUB_PID=$! + + # New package substitution + ( + cd new/ + if [ -d "$REAL_FOLDER_NAME" ] && [ "$REAL_FOLDER_NAME" != "google" ]; then + mv "$REAL_FOLDER_NAME" google + fi + local fake_package_name=google/provider/new + + if [ "$(uname)" = "Darwin" ]; then + find . -type f -name "*.go" -exec sed -i "" "s~${REAL_PACKAGE_NAME}/${REAL_FOLDER_NAME}~${fake_package_name}/google~g" {} + + sed -i "" "s|${REAL_PACKAGE_NAME}|${fake_package_name}|g" go.mod + else + find . -type f -name "*.go" -exec sed -i "s~${REAL_PACKAGE_NAME}/${REAL_FOLDER_NAME}~${fake_package_name}/google~g" {} + + sed -i "s|${REAL_PACKAGE_NAME}|${fake_package_name}|g" go.mod + fi + ) & + local NEW_SUB_PID=$! + + wait $OLD_SUB_PID + wait $NEW_SUB_PID + + # Tidy and build diff-processor + echo -e "${BLUE}[${VERSION_UPPER}] Compiling diff-processor tool...${NC}" + go mod tidy >/dev/null 2>&1 + mkdir -p bin/ + go build -o ./bin/diff-processor . + ) + + local HAS_ERR=0 + + # Run breaking changes check + echo -e "\n${BLUE}=== Running Breaking Changes Check (${VERSION_UPPER}) ===${NC}" + set +e + local BREAKING_OUTPUT=$("$TARGET_DIFF_PROC/bin/diff-processor" breaking-changes 2>&1) + local BREAKING_EXIT_CODE=$? + set -e + + if [ $BREAKING_EXIT_CODE -ne 0 ]; then + echo -e "${RED}[${VERSION_UPPER}] Error: diff-processor breaking-changes failed:${NC}" + echo "$BREAKING_OUTPUT" + return $BREAKING_EXIT_CODE + fi + + if [ -n "$BREAKING_OUTPUT" ] && [ "$BREAKING_OUTPUT" != "[]" ] && [ "$BREAKING_OUTPUT" != "null" ]; then + echo -e "${RED}[${VERSION_UPPER}] Breaking changes detected!${NC}" + if command -v jq &> /dev/null; then + echo "$BREAKING_OUTPUT" | jq . + else + echo "$BREAKING_OUTPUT" | python3 -m json.tool + fi + HAS_ERR=1 + else + echo -e "${GREEN}[${VERSION_UPPER}] No breaking changes detected!${NC}" + fi + + # Run missing tests check + echo -e "\n${BLUE}=== Running Missing Tests Check (${VERSION_UPPER}) ===${NC}" + set +e + local TESTS_OUTPUT=$("$TARGET_DIFF_PROC/bin/diff-processor" detect-missing-tests "$TARGET_DIFF_PROC/new/google/services" 2>&1) + local TESTS_EXIT_CODE=$? + set -e + + if [ $TESTS_EXIT_CODE -ne 0 ]; then + echo -e "${RED}[${VERSION_UPPER}] Error: diff-processor detect-missing-tests failed:${NC}" + echo "$TESTS_OUTPUT" + return $TESTS_EXIT_CODE + fi + + if [ -n "$TESTS_OUTPUT" ] && [ "$TESTS_OUTPUT" != "{}" ] && [ "$TESTS_OUTPUT" != "null" ]; then + echo -e "${YELLOW}[${VERSION_UPPER}] Missing tests detected!${NC}" + if command -v jq &> /dev/null; then + echo "$TESTS_OUTPUT" | jq . + else + echo "$TESTS_OUTPUT" | python3 -m json.tool + fi + HAS_ERR=1 + else + echo -e "${GREEN}[${VERSION_UPPER}] No missing tests detected!${NC}" + fi + + # Run missing documentation check + echo -e "\n${BLUE}=== Running Missing Documentation Check (${VERSION_UPPER}) ===${NC}" + set +e + local DOCS_OUTPUT=$("$TARGET_DIFF_PROC/bin/diff-processor" detect-missing-docs "$TARGET_DIFF_PROC/new" 2>&1) + local DOCS_EXIT_CODE=$? + set -e + + if [ $DOCS_EXIT_CODE -ne 0 ]; then + echo -e "${RED}[${VERSION_UPPER}] Error: diff-processor detect-missing-docs failed:${NC}" + echo "$DOCS_OUTPUT" + return $DOCS_EXIT_CODE + fi + + if has_missing_docs "$DOCS_OUTPUT"; then + echo -e "${YELLOW}[${VERSION_UPPER}] Missing documentation detected!${NC}" + if command -v jq &> /dev/null; then + echo "$DOCS_OUTPUT" | jq . + else + echo "$DOCS_OUTPUT" | python3 -m json.tool + fi + HAS_ERR=1 + else + echo -e "${GREEN}[${VERSION_UPPER}] No missing documentation detected!${NC}" + fi + + return $HAS_ERR +} + +echo -e "${BLUE}Running GA and Beta schema diff checks in parallel...${NC}" + +run_version_checks ga > "$SCRATCH_DIR/check-ga.log" 2>&1 & +GA_PID=$! + +run_version_checks beta > "$SCRATCH_DIR/check-beta.log" 2>&1 & +BETA_PID=$! + +set +e +wait $GA_PID +GA_STATUS=$? + +wait $BETA_PID +BETA_STATUS=$? +set -e + +echo -e "\n${BLUE}=====================================================${NC}" +echo -e "${BLUE}=== GA Provider Schema Diff Results ===${NC}" +echo -e "${BLUE}=====================================================${NC}" +cat "$SCRATCH_DIR/check-ga.log" + +echo -e "\n${BLUE}=====================================================${NC}" +echo -e "${BLUE}=== Beta Provider Schema Diff Results ===${NC}" +echo -e "${BLUE}=====================================================${NC}" +cat "$SCRATCH_DIR/check-beta.log" + +# 7. Final status report +echo -e "\n${BLUE}===========================================${NC}" +if [ $GA_STATUS -ne 0 ] || [ $BETA_STATUS -ne 0 ]; then + echo -e "${RED}=== Schema change checks failed! ===${NC}" + exit 1 +else + echo -e "${GREEN}=== All schema change checks passed successfully! ===${NC}" + exit 0 +fi diff --git a/.gitignore b/.gitignore index 15f0bf57c472..5bdfea630dab 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ *# *.swp *.bak +/scratch/ # Python *.pyc From 270125be0b2b32568b4bfebbd06d7e150778a745 Mon Sep 17 00:00:00 2001 From: Baruch Steinberg Date: Mon, 6 Jul 2026 21:49:28 +0300 Subject: [PATCH 32/78] =?UTF-8?q?Align=20Filestore=20instance=20opeation?= =?UTF-8?q?=20timeouts=20with=20the=20service=20defined=20t=E2=80=A6=20(#1?= =?UTF-8?q?8146)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mmv1/products/filestore/Instance.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mmv1/products/filestore/Instance.yaml b/mmv1/products/filestore/Instance.yaml index 0490eaaa89de..2a3902406196 100644 --- a/mmv1/products/filestore/Instance.yaml +++ b/mmv1/products/filestore/Instance.yaml @@ -28,9 +28,9 @@ create_url: projects/{{project}}/locations/{{location}}/instances?instanceId={{n update_mask: true update_verb: PATCH timeouts: - insert_minutes: 20 - update_minutes: 20 - delete_minutes: 20 + insert_minutes: 14400 + update_minutes: 120 + delete_minutes: 120 sweeper: url_substitutions: - region: us-central1-b From 21357a4fa15e7f1f021475fcc7b4fd1c3191e20a Mon Sep 17 00:00:00 2001 From: wrathtoliar Date: Tue, 7 Jul 2026 00:27:11 +0530 Subject: [PATCH 33/78] Time_sleep resource is added to wait for 120 seconds after the application is created, to allow for IAM permission propagation before creating the firewall rule. (#18182) --- mmv1/products/appengine/FirewallRule.yaml | 1 + .../appengine/app_engine_firewall_rule_basic.tf.tmpl | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/mmv1/products/appengine/FirewallRule.yaml b/mmv1/products/appengine/FirewallRule.yaml index 8c945d43865a..6317d4e5a68d 100644 --- a/mmv1/products/appengine/FirewallRule.yaml +++ b/mmv1/products/appengine/FirewallRule.yaml @@ -45,6 +45,7 @@ custom_code: samples: - name: app_engine_firewall_rule_basic primary_resource_id: rule + external_providers: [time] steps: - name: app_engine_firewall_rule_basic resource_id_vars: diff --git a/mmv1/templates/terraform/samples/services/appengine/app_engine_firewall_rule_basic.tf.tmpl b/mmv1/templates/terraform/samples/services/appengine/app_engine_firewall_rule_basic.tf.tmpl index fda913e535f2..79c3e9815581 100644 --- a/mmv1/templates/terraform/samples/services/appengine/app_engine_firewall_rule_basic.tf.tmpl +++ b/mmv1/templates/terraform/samples/services/appengine/app_engine_firewall_rule_basic.tf.tmpl @@ -11,9 +11,18 @@ resource "google_app_engine_application" "app" { location_id = "us-central" } +# Wait for 5 minutes seconds to ensure IAM permission propagation completes +resource "time_sleep" "wait_300_seconds" { + depends_on = [google_app_engine_application.app] + create_duration = "300s" +} + + resource "google_app_engine_firewall_rule" "rule" { project = google_app_engine_application.app.project priority = 1000 action = "ALLOW" source_range = "*" + + depends_on = [time_sleep.wait_300_seconds] } From 2b1d2739c4b10ac68d96cc67365f3357c8128ac8 Mon Sep 17 00:00:00 2001 From: gurusai-voleti Date: Tue, 7 Jul 2026 01:11:23 +0530 Subject: [PATCH 34/78] feat: (storage) deprecated cache value (#18083) --- mmv1/products/storage/AnywhereCache.yaml | 9 ++++++-- .../constants/storage_anywhere_cache.go.tmpl | 21 +++++++++++++++++++ .../resource_storage_anywhere_cache_test.go | 3 +++ 3 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 mmv1/templates/terraform/constants/storage_anywhere_cache.go.tmpl diff --git a/mmv1/products/storage/AnywhereCache.yaml b/mmv1/products/storage/AnywhereCache.yaml index 0f1f6bc20429..c1fe3b1003b4 100644 --- a/mmv1/products/storage/AnywhereCache.yaml +++ b/mmv1/products/storage/AnywhereCache.yaml @@ -43,6 +43,7 @@ async: resource_inside_response: true autogen_async: false custom_code: + constants: templates/terraform/constants/storage_anywhere_cache.go.tmpl custom_create: templates/terraform/custom_create/storage_anywhere_cache.go.tmpl custom_update: templates/terraform/custom_update/storage_anywhere_cache.go.tmpl samples: @@ -71,11 +72,15 @@ properties: description: The zone in which the cache instance needs to be created. For example, `us-central1-a.` - name: admissionPolicy type: Enum + default_value: admit-on-first-miss + deprecation_message: '`admit-on-second-miss` is deprecated and will be removed in a future major release. The backend will ignore this attribute and treat it as `admit-on-first-miss`.' + description: | + The cache admission policy dictates whether a block should be inserted upon a cache miss. + Note: "admit-on-second-miss" is deprecated and will fallback to "admit-on-first-miss". enum_values: - admit-on-first-miss - admit-on-second-miss - default_value: admit-on-first-miss - description: The cache admission policy dictates whether a block should be inserted upon a cache miss. + diff_suppress_func: StorageAnywhereCacheAdmissionPolicyDiffSuppress - name: ttl type: String default_value: 86400s diff --git a/mmv1/templates/terraform/constants/storage_anywhere_cache.go.tmpl b/mmv1/templates/terraform/constants/storage_anywhere_cache.go.tmpl new file mode 100644 index 000000000000..a1907c17b391 --- /dev/null +++ b/mmv1/templates/terraform/constants/storage_anywhere_cache.go.tmpl @@ -0,0 +1,21 @@ +{{/* + The license inside this block applies to this file + 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. +*/ -}} +// Suppresses diffs for Anywhere Cache admission policy where "admit-on-second-miss" +// is deprecated and falls back to "admit-on-first-miss". +func StorageAnywhereCacheAdmissionPolicyDiffSuppress(_, old, new string, _ *schema.ResourceData) bool { + if old == new { + return true + } + return (old == "admit-on-first-miss" && new == "admit-on-second-miss") || + (old == "admit-on-second-miss" && new == "admit-on-first-miss") +} diff --git a/mmv1/third_party/terraform/services/storage/resource_storage_anywhere_cache_test.go b/mmv1/third_party/terraform/services/storage/resource_storage_anywhere_cache_test.go index ede31a228673..d37d046f4301 100644 --- a/mmv1/third_party/terraform/services/storage/resource_storage_anywhere_cache_test.go +++ b/mmv1/third_party/terraform/services/storage/resource_storage_anywhere_cache_test.go @@ -39,6 +39,9 @@ func TestAccStorageAnywhereCache_update(t *testing.T) { plancheck.ExpectResourceAction("google_storage_anywhere_cache.cache", plancheck.ResourceActionUpdate), }, }, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_storage_anywhere_cache.cache", "admission_policy", "admit-on-first-miss"), + ), }, { ResourceName: "google_storage_anywhere_cache.cache", From 9f0cdbf0920a4747b967ffeef98cc8e2feeb7ace Mon Sep 17 00:00:00 2001 From: shellyvilenko <144919183+shellyvilenko@users.noreply.github.com> Date: Mon, 6 Jul 2026 15:57:08 -0400 Subject: [PATCH 35/78] Update ManagedZone.yaml to contain ransom suffix for dns_name (#18086) --- mmv1/products/dns/ManagedZone.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mmv1/products/dns/ManagedZone.yaml b/mmv1/products/dns/ManagedZone.yaml index 31c0b398a48f..eaf4ff763a90 100644 --- a/mmv1/products/dns/ManagedZone.yaml +++ b/mmv1/products/dns/ManagedZone.yaml @@ -55,7 +55,7 @@ samples: example_zone_googlecloudexample: example-zone-googlecloudexample dns_name: googlecloudexample.net. test_vars_overrides: - dns_name: '"m-z.gcp.tfacc.hashicorptest.com."' + dns_name: '"m-z-" + randomSuffix + ".gcp.tfacc.hashicorptest.com."' ignore_read_extra: - force_destroy - name: dns_record_set_basic From dda05b19f0bc90e0c3d83af740a51ec0f8bb5d98 Mon Sep 17 00:00:00 2001 From: Riddhi Shivhare Date: Tue, 7 Jul 2026 01:58:58 +0530 Subject: [PATCH 36/78] appengine: Add app_engine_bundled_services and make fields immutable (#18062) --- .../appengine/StandardAppVersion.yaml | 44 +++++ ...ndard_app_version_bundled_services.tf.tmpl | 65 ++++++ ...ce_app_engine_standard_app_version_test.go | 186 +++++++++++++++++- 3 files changed, 288 insertions(+), 7 deletions(-) create mode 100644 mmv1/templates/terraform/samples/services/appengine/app_engine_standard_app_version_bundled_services.tf.tmpl diff --git a/mmv1/products/appengine/StandardAppVersion.yaml b/mmv1/products/appengine/StandardAppVersion.yaml index 93c60a6f7f2e..e8ab443f16f1 100644 --- a/mmv1/products/appengine/StandardAppVersion.yaml +++ b/mmv1/products/appengine/StandardAppVersion.yaml @@ -74,6 +74,17 @@ samples: org_id: ORG_ID ignore_read_extra: - delete_service_on_destroy + - name: app_engine_standard_app_version_bundled_services + primary_resource_id: gae-std-app-ver-bundled + steps: + - name: app_engine_standard_app_version_bundled_services + resource_id_vars: + project_id: tf-test-project + service_id: bundled-service + bucket_name: tf-test-gae-bkt-bundled + sa_email: gae-sa + ignore_read_extra: + - delete_service_on_destroy virtual_fields: - name: noop_on_destroy description: | @@ -125,6 +136,10 @@ properties: type: Boolean description: | Allows App Engine second generation runtimes to access the legacy bundled services. + Cannot specify both `app_engine_apis` and 'app_engine_bundled_services` together. + immutable: true + conflicts: + - app_engine_bundled_services - name: runtimeApiVersion type: String description: | @@ -443,3 +458,32 @@ properties: **Note:** When managing the number of instances at runtime through the App Engine Admin API or the (now deprecated) Python 2 Modules API set_num_instances() you must use `lifecycle.ignore_changes = ["manual_scaling"[0].instances]` to prevent drift detection. required: true + - name: 'appEngineBundledServices' + type: Array + # Developer Note: This field is write-only because the App Engine GET Admin API + # does not return it in the response (we use ignore_read: true to prevent state drift to null). + # It is marked is_set: true to represent the list as a set in Terraform so configuration + # ordering differences do not trigger spurious plans. + description: | + A list of legacy bundled services to enable for this version on an App Engine second-generation runtime. + Cannot specify both `app_engine_apis` and `app_engine_bundled_services` together. + is_set: true + immutable: true + ignore_read: true + item_type: + type: Enum + enum_values: + - 'BUNDLED_SERVICE_TYPE_APP_IDENTITY_SERVICE' + - 'BUNDLED_SERVICE_TYPE_BLOBSTORE' + - 'BUNDLED_SERVICE_TYPE_CAPABILITY_SERVICE' + - 'BUNDLED_SERVICE_TYPE_DATASTORE_V3' + - 'BUNDLED_SERVICE_TYPE_IMAGES' + - 'BUNDLED_SERVICE_TYPE_MAIL' + - 'BUNDLED_SERVICE_TYPE_MEMCACHE' + - 'BUNDLED_SERVICE_TYPE_MODULES' + - 'BUNDLED_SERVICE_TYPE_SEARCH' + - 'BUNDLED_SERVICE_TYPE_TASKQUEUES' + - 'BUNDLED_SERVICE_TYPE_URLFETCH' + - 'BUNDLED_SERVICE_TYPE_USERS' + conflicts: + - app_engine_apis diff --git a/mmv1/templates/terraform/samples/services/appengine/app_engine_standard_app_version_bundled_services.tf.tmpl b/mmv1/templates/terraform/samples/services/appengine/app_engine_standard_app_version_bundled_services.tf.tmpl new file mode 100644 index 000000000000..3af2dc69e1e9 --- /dev/null +++ b/mmv1/templates/terraform/samples/services/appengine/app_engine_standard_app_version_bundled_services.tf.tmpl @@ -0,0 +1,65 @@ +resource "google_service_account" "service_account" { + account_id = "{{index $.ResourceIdVars "sa_email"}}" + display_name = "Test Service Account for GAE" +} + +resource "google_project_iam_member" "gae_api" { + project = google_service_account.service_account.project + role = "roles/compute.networkUser" + member = "serviceAccount:${google_service_account.service_account.email}" +} + +resource "google_project_iam_member" "storage_viewer" { + project = google_service_account.service_account.project + role = "roles/storage.objectViewer" + member = "serviceAccount:${google_service_account.service_account.email}" +} + +resource "google_storage_bucket" "bucket" { + name = "{{index $.ResourceIdVars "bucket_name"}}" + location = "US" +} + +resource "google_storage_bucket_object" "requirements" { + name = "requirements.txt" + bucket = google_storage_bucket.bucket.name + source = "./test-fixtures/hello-world-flask/requirements.txt" +} + +resource "google_storage_bucket_object" "main" { + name = "main.py" + bucket = google_storage_bucket.bucket.name + source = "./test-fixtures/hello-world-flask/main.py" +} + +resource "google_app_engine_standard_app_version" "{{$.PrimaryResourceId}}" { + version_id = "v1" + service = "{{index $.ResourceIdVars "service_id"}}" + runtime = "python310" + + deployment { + files { + name = "main.py" + source_url = "https://storage.googleapis.com/${google_storage_bucket.bucket.name}/${google_storage_bucket_object.main.name}" + } + files { + name = "requirements.txt" + source_url = "https://storage.googleapis.com/${google_storage_bucket.bucket.name}/${google_storage_bucket_object.requirements.name}" + } + } + + entrypoint { + shell = "gunicorn -b :$PORT main:app" + } + +# Testing the app_engine_bundled_services field + app_engine_bundled_services = ["BUNDLED_SERVICE_TYPE_MAIL", "BUNDLED_SERVICE_TYPE_DATASTORE_V3"] + + delete_service_on_destroy = true + service_account = google_service_account.service_account.email + + depends_on = [ + google_project_iam_member.gae_api, + google_project_iam_member.storage_viewer, + ] +} diff --git a/mmv1/third_party/terraform/services/appengine/resource_app_engine_standard_app_version_test.go b/mmv1/third_party/terraform/services/appengine/resource_app_engine_standard_app_version_test.go index 93bb97e26c74..6c60e2eb0881 100644 --- a/mmv1/third_party/terraform/services/appengine/resource_app_engine_standard_app_version_test.go +++ b/mmv1/third_party/terraform/services/appengine/resource_app_engine_standard_app_version_test.go @@ -6,10 +6,6 @@ import ( "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-provider-google/google/acctest" "github.com/hashicorp/terraform-provider-google/google/envvar" - _ "github.com/hashicorp/terraform-provider-google/google/services/appengine" - _ "github.com/hashicorp/terraform-provider-google/google/services/resourcemanager" - _ "github.com/hashicorp/terraform-provider-google/google/services/storage" - _ "github.com/hashicorp/terraform-provider-google/google/services/vpcaccess" ) func TestAccAppEngineStandardAppVersion_update(t *testing.T) { @@ -87,7 +83,7 @@ resource "google_app_engine_standard_app_version" "foo" { project = google_project_service.project.project version_id = "v1" service = "default" - runtime = "python38" + runtime = "python310" entrypoint { shell = "gunicorn -b :$PORT main:app" @@ -204,7 +200,7 @@ resource "google_app_engine_standard_app_version" "foo" { project = google_project_service.project.project version_id = "v1" service = "default" - runtime = "python38" + runtime = "python310" vpc_access_connector { name = "${google_vpc_access_connector.bar.id}" @@ -297,7 +293,7 @@ resource "google_app_engine_standard_app_version" "foo" { project = google_project_service.project.project version_id = "v1" service = "default" - runtime = "python38" + runtime = "python310" entrypoint { shell = "gunicorn -b :$PORT main:app" @@ -348,3 +344,179 @@ resource "google_storage_bucket_object" "main" { source = "./test-fixtures/hello-world-flask/main.py" }`, context) } + +func TestAccAppEngineStandardAppVersion_updateBundledServices(t *testing.T) { + t.Skip("https://github.com/hashicorp/terraform-provider-google/issues/18936") + t.Parallel() + + context := map[string]interface{}{ + "org_id": envvar.GetTestOrgFromEnv(t), + "billing_account": envvar.GetTestBillingAccountFromEnv(t), + "random_suffix": acctest.RandString(t, 10), + } + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + ExternalProviders: map[string]resource.ExternalProvider{ + "time": {}, + }, + CheckDestroy: testAccCheckAppEngineStandardAppVersionDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccAppEngineStandardAppVersion_bundledServices(context), + }, + { + ResourceName: "google_app_engine_standard_app_version.foo", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"env_variables", "deployment", "entrypoint", "service", "noop_on_destroy"}, + }, + { + Config: testAccAppEngineStandardAppVersion_bundledServicesUpdate(context), + }, + { + ResourceName: "google_app_engine_standard_app_version.foo", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"env_variables", "deployment", "entrypoint", "service", "noop_on_destroy"}, + }, + }, + }) +} + +func testAccAppEngineStandardAppVersion_bundledServices(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_project" "my_project" { + name = "tf-test-appeng-std%{random_suffix}" + project_id = "tf-test-appeng-std%{random_suffix}" + org_id = "%{org_id}" + billing_account = "%{billing_account}" + deletion_policy = "DELETE" +} + +resource "google_app_engine_application" "app" { + project = google_project.my_project.project_id + location_id = "us-central" +} + +resource "google_project_service" "project" { + project = google_project.my_project.project_id + service = "appengine.googleapis.com" + disable_dependent_services = false +} + +resource "google_app_engine_standard_app_version" "foo" { + project = google_project_service.project.project + version_id = "v1" + service = "default" + runtime = "python310" + + app_engine_bundled_services = ["BUNDLED_SERVICE_TYPE_MAIL", "BUNDLED_SERVICE_TYPE_USERS"] + + entrypoint { + shell = "gunicorn -b :$PORT main:app" + } + + deployment { + files { + name = "main.py" + source_url = "https://storage.googleapis.com/${google_storage_bucket.bucket.name}/${google_storage_bucket_object.main.name}" + } + files { + name = "requirements.txt" + source_url = "https://storage.googleapis.com/${google_storage_bucket.bucket.name}/${google_storage_bucket_object.requirements.name}" + } + } + + instance_class = "F2" + noop_on_destroy = true +} + +resource "google_storage_bucket" "bucket" { + project = google_project.my_project.project_id + name = "tf-test-%{random_suffix}-standard-ae-bucket" + location = "US" +} + +resource "google_storage_bucket_object" "requirements" { + name = "requirements.txt" + bucket = google_storage_bucket.bucket.name + source = "./test-fixtures/hello-world-flask/requirements.txt" +} + +resource "google_storage_bucket_object" "main" { + name = "main.py" + bucket = google_storage_bucket.bucket.name + source = "./test-fixtures/hello-world-flask/main.py" +} +`, context) +} + +func testAccAppEngineStandardAppVersion_bundledServicesUpdate(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_project" "my_project" { + name = "tf-test-appeng-std%{random_suffix}" + project_id = "tf-test-appeng-std%{random_suffix}" + org_id = "%{org_id}" + billing_account = "%{billing_account}" + deletion_policy = "DELETE" +} + +resource "google_app_engine_application" "app" { + project = google_project.my_project.project_id + location_id = "us-central" +} + +resource "google_project_service" "project" { + project = google_project.my_project.project_id + service = "appengine.googleapis.com" + disable_dependent_services = false +} + +resource "google_app_engine_standard_app_version" "foo" { + project = google_project_service.project.project + version_id = "v1" + service = "default" + runtime = "python310" + + app_engine_bundled_services = ["mail", "user"] + + entrypoint { + shell = "gunicorn -b :$PORT main:app" + } + + deployment { + files { + name = "main.py" + source_url = "https://storage.googleapis.com/${google_storage_bucket.bucket.name}/${google_storage_bucket_object.main.name}" + } + files { + name = "requirements.txt" + source_url = "https://storage.googleapis.com/${google_storage_bucket.bucket.name}/${google_storage_bucket_object.requirements.name}" + } + } + + instance_class = "F2" + noop_on_destroy = true +} + +resource "google_storage_bucket" "bucket" { + project = google_project.my_project.project_id + name = "tf-test-%{random_suffix}-standard-ae-bucket" + location = "US" +} + +resource "google_storage_bucket_object" "requirements" { + name = "requirements.txt" + bucket = google_storage_bucket.bucket.name + source = "./test-fixtures/hello-world-flask/requirements.txt" +} + +resource "google_storage_bucket_object" "main" { + name = "main.py" + bucket = google_storage_bucket.bucket.name + source = "./test-fixtures/hello-world-flask/main.py" +} +`, context) +} From ad5550e4313d4488c956be81ece1dde03b2fb839 Mon Sep 17 00:00:00 2001 From: Riley Karson Date: Mon, 6 Jul 2026 14:29:37 -0700 Subject: [PATCH 37/78] Add service team mandatory review (#18191) --- docs/content/code-review/create-pr.md | 3 ++- docs/content/code-review/release-notes.md | 2 +- docs/content/code-review/review-pr.md | 19 ++++++++++++------- docs/content/contribution-process.md | 3 ++- 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/docs/content/code-review/create-pr.md b/docs/content/code-review/create-pr.md index 698b21df9968..07a59f2de4f6 100644 --- a/docs/content/code-review/create-pr.md +++ b/docs/content/code-review/create-pr.md @@ -18,10 +18,11 @@ aliases: ## Code review -1. A reviewer will automatically be assigned to your PR. +1. (For Google-driven changes) Assign a service team engineer to review your change. Get their approval before proceeding with review by maintainers. 1. Creating a new pull request or pushing a new commit automatically triggers our CI pipelines and workflows. After CI starts, downstream diff generation takes about 10 minutes; [VCR tests]({{< ref "/test/test" >}}) can take up to 2 hours. If you are a community contributor, some tests will only run after approval from a reviewer. - While convenient, relying on CI to test iterative changes to PRs often adds extreme latency to reviews if there are errors in test configurations or at runtime. We **strongly** recommend you [test your changes locally before pushing]({{< ref "/test/run-tests" >}}) even after the initial change. - VCR tests will first attempt to play back recorded HTTP requests (REPLAYING mode). If any tests fail, they will run in RECORDING mode to generate a new cassette; then, the same tests will run again in REPLAYING mode to detect any nondeterministic behavior in the test (which can cause flaky tests.) +1. A maintainer will automatically be assigned to your PR for final review. 1. If your assigned reviewer does not respond to changes on a pull request within two US business days, ping them on the pull request. > [!NOTE] diff --git a/docs/content/code-review/release-notes.md b/docs/content/code-review/release-notes.md index 13e26272f2d5..eb469b5fa5f0 100644 --- a/docs/content/code-review/release-notes.md +++ b/docs/content/code-review/release-notes.md @@ -1,6 +1,6 @@ --- title: "Write release notes" -weight: 20 +weight: 30 aliases: - /contribute/release-notes --- diff --git a/docs/content/code-review/review-pr.md b/docs/content/code-review/review-pr.md index 3a2ed0a086cf..19686de2a683 100644 --- a/docs/content/code-review/review-pr.md +++ b/docs/content/code-review/review-pr.md @@ -1,6 +1,6 @@ --- title: "Review a pull request" -weight: 30 +weight: 20 aliases: - /contribute/review-pr --- @@ -9,12 +9,8 @@ aliases: This page provides guidelines for reviewing a Magic Modules pull request (PR). -## Special cases - -The following types of PRs may require additional scrutiny and/or multiple reviewers. - -- DCL to MMv1 migrations -- Adding multi-actor fields (fields whose values can be altered as a side effect of changes made to a different resource) +Google-driven changes must have a service team engineer review changes prior +to review by repository maintainers. For community members, this is not required. ## Review @@ -58,3 +54,12 @@ The following types of PRs may require additional scrutiny and/or multiple revie 1. resource-level and field-level documentation are generated correctly for MMv1-based resource 1. documentation is added manually for handwritten resources. 1. Check if release notes capture all changes in the PR, and are correctly formatted following the guidance in [write release notes]({{< ref "release-notes" >}}) before merging the PR. + +If any of these steps are impossible, clearly explain that in a comment on the PR or in a summary before handing off to a maintainer for final review. + +## Special cases + +The following types of PRs may require additional scrutiny and/or multiple reviewers. + +- DCL to MMv1 migrations +- Adding multi-actor fields (fields whose values can be altered as a side effect of changes made to a different resource) \ No newline at end of file diff --git a/docs/content/contribution-process.md b/docs/content/contribution-process.md index 81a75a8a32fc..159a51539336 100644 --- a/docs/content/contribution-process.md +++ b/docs/content/contribution-process.md @@ -33,7 +33,8 @@ This page explains how you can contribute code and documentation to the `magic-m 1. [Generate the providers]({{< ref "/develop/generate-providers" >}}) that include your change. 1. [Run provider tests locally]({{< ref "/test/run-tests" >}}) that are relevant to the change you made 1. [Create a pull request (PR)]({{< ref "/code-review/create-pr" >}}) -1. Make changes in response to [code review]({{< ref "/code-review/create-pr#code-review" >}}) +1. (For Google-driven changes) Do a [local review]({{< ref "/code-review/review-pr" >}}) with a service team engineer +1. Make changes in response to maintainers' [code review]({{< ref "/code-review/create-pr#code-review" >}}) ## After your change is merged From 924817bac62e72fd0235f6daa8f5a01711a50362 Mon Sep 17 00:00:00 2001 From: Enden Date: Tue, 7 Jul 2026 01:19:51 +0300 Subject: [PATCH 38/78] Add google_chronicle_soardomain resource to chronicle (#17714) Co-authored-by: Eden Shalit --- mmv1/products/chronicle/SoarDomain.yaml | 79 +++++++++++++++++++ .../chronicle_soardomain_basic.tf.tmpl | 8 ++ .../chronicle_soardomain_full.tf.tmpl | 8 ++ 3 files changed, 95 insertions(+) create mode 100644 mmv1/products/chronicle/SoarDomain.yaml create mode 100644 mmv1/templates/terraform/samples/services/chronicle/chronicle_soardomain_basic.tf.tmpl create mode 100644 mmv1/templates/terraform/samples/services/chronicle/chronicle_soardomain_full.tf.tmpl diff --git a/mmv1/products/chronicle/SoarDomain.yaml b/mmv1/products/chronicle/SoarDomain.yaml new file mode 100644 index 000000000000..04bf2e80a518 --- /dev/null +++ b/mmv1/products/chronicle/SoarDomain.yaml @@ -0,0 +1,79 @@ +# 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: SoarDomain +description: > + A SOAR domain designate a domain as internal, ensuring that entities associated with it are treated as organizational assets rather than external threats during ingestion. +min_version: 'beta' +references: + guides: + 'Google SecOps Guides': 'https://cloud.google.com/chronicle/docs/secops/secops-overview' + api: 'https://docs.cloud.google.com/chronicle/docs/reference/rest/v1beta/projects.locations.instances.soarDomains' +base_url: projects/{{project}}/locations/{{location}}/instances/{{instance}}/soarDomains +self_link: projects/{{project}}/locations/{{location}}/instances/{{instance}}/soarDomains/{{soar_domain_id}} +create_url: projects/{{project}}/locations/{{location}}/instances/{{instance}}/soarDomains +id_format: projects/{{project}}/locations/{{location}}/instances/{{instance}}/soarDomains/{{soar_domain_id}} +update_mask: true +update_verb: PATCH +import_format: + - projects/{{project}}/locations/{{location}}/instances/{{instance}}/soarDomains/{{soar_domain_id}} +autogen_status: U29hckRvbWFpbg== + +samples: + - name: chronicle_soardomain_update + primary_resource_id: example + min_version: beta + steps: + - name: "chronicle_soardomain_basic" + test_env_vars: + chronicle_id: 'CHRONICLE_ID' + - name: "chronicle_soardomain_full" + test_env_vars: + chronicle_id: 'CHRONICLE_ID' + +parameters: + - name: location + type: String + required: true + 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 + - name: instance + type: String + required: true + 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 + - name: soarDomainId + type: String + output: true + custom_flatten: 'templates/terraform/custom_flatten/id_from_name.tmpl' + description: |- + Id of the domain record. +properties: + - name: displayName + type: String + required: true + description: SoarDomain display name, limited to 4096 characters. + - name: environmentsJson + type: String + required: true + description: SoarDomain associated logical environments (json). + - name: name + type: String + description: |- + Identifier. The unique name(ID) of the SoarDomain. + Format: + projects/{project}/locations/{location}/instances/{instance}/soarDomains/{soar_domain_id} + output: true diff --git a/mmv1/templates/terraform/samples/services/chronicle/chronicle_soardomain_basic.tf.tmpl b/mmv1/templates/terraform/samples/services/chronicle/chronicle_soardomain_basic.tf.tmpl new file mode 100644 index 000000000000..e2c3afa3190b --- /dev/null +++ b/mmv1/templates/terraform/samples/services/chronicle/chronicle_soardomain_basic.tf.tmpl @@ -0,0 +1,8 @@ +resource "google_chronicle_soar_domain" "{{$.PrimaryResourceId}}" { + provider = google-beta + location = "us" + instance = "{{index $.TestEnvVars "chronicle_id"}}" + + display_name = "test.com" + environments_json = "[\"Default Environment\"]" +} diff --git a/mmv1/templates/terraform/samples/services/chronicle/chronicle_soardomain_full.tf.tmpl b/mmv1/templates/terraform/samples/services/chronicle/chronicle_soardomain_full.tf.tmpl new file mode 100644 index 000000000000..34e07c139aaf --- /dev/null +++ b/mmv1/templates/terraform/samples/services/chronicle/chronicle_soardomain_full.tf.tmpl @@ -0,0 +1,8 @@ +resource "google_chronicle_soar_domain" "{{$.PrimaryResourceId}}" { + provider = google-beta + location = "us" + instance = "{{index $.TestEnvVars "chronicle_id"}}" + + display_name = "test1.com" + environments_json = "[\"*\"]" +} From 7c55e19b74e9a80907c8f591088aa266496ee6a5 Mon Sep 17 00:00:00 2001 From: Gregory Colella <4230373+gdcolella@users.noreply.github.com> Date: Mon, 6 Jul 2026 18:52:59 -0400 Subject: [PATCH 39/78] Fix row_affinity configuration in Bigtable app profiles (#18031) --- .../pre_update/bigtable_app_profile.go.tmpl | 2 +- .../resource_bigtable_app_profile_test.go | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/mmv1/templates/terraform/pre_update/bigtable_app_profile.go.tmpl b/mmv1/templates/terraform/pre_update/bigtable_app_profile.go.tmpl index 307208274cb2..6f325ef59628 100644 --- a/mmv1/templates/terraform/pre_update/bigtable_app_profile.go.tmpl +++ b/mmv1/templates/terraform/pre_update/bigtable_app_profile.go.tmpl @@ -11,7 +11,7 @@ limitations under the License. */}} -if d.HasChange("multi_cluster_routing_cluster_ids") && !tpgresource.StringInSlice(updateMask, "multiClusterRoutingUseAny") { +if (d.HasChange("multi_cluster_routing_cluster_ids") || d.HasChange("row_affinity")) && !tpgresource.StringInSlice(updateMask, "multiClusterRoutingUseAny") { updateMask = append(updateMask, "multiClusterRoutingUseAny") } diff --git a/mmv1/third_party/terraform/services/bigtable/resource_bigtable_app_profile_test.go b/mmv1/third_party/terraform/services/bigtable/resource_bigtable_app_profile_test.go index 483f73bf2a54..c0c3a8da05de 100644 --- a/mmv1/third_party/terraform/services/bigtable/resource_bigtable_app_profile_test.go +++ b/mmv1/third_party/terraform/services/bigtable/resource_bigtable_app_profile_test.go @@ -633,6 +633,24 @@ func TestAccBigtableAppProfile_updateRowAffinity(t *testing.T) { ImportStateVerify: true, ImportStateVerifyIgnore: []string{"ignore_warnings"}, }, + { + Config: testAccBigtableAppProfile_updateMC1(instanceName), + }, + { + ResourceName: "google_bigtable_app_profile.ap", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"ignore_warnings"}, + }, + { + Config: testAccBigtableAppProfile_updateRowAffinity(instanceName), + }, + { + ResourceName: "google_bigtable_app_profile.ap", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"ignore_warnings"}, + }, { Config: testAccBigtableAppProfile_update1(instanceName), }, From 291e71b923b201316cdcd3beb55f58a5856d9618 Mon Sep 17 00:00:00 2001 From: tsushanth <78000697+tsushanth@users.noreply.github.com> Date: Mon, 6 Jul 2026 15:57:21 -0700 Subject: [PATCH 40/78] google_chronicle_rule: suppress trailing-newline permadiff on `text` (#17991) --- mmv1/products/chronicle/Rule.yaml | 2 ++ mmv1/templates/terraform/constants/chronicle_rule.go.tmpl | 8 ++++++++ 2 files changed, 10 insertions(+) create mode 100644 mmv1/templates/terraform/constants/chronicle_rule.go.tmpl diff --git a/mmv1/products/chronicle/Rule.yaml b/mmv1/products/chronicle/Rule.yaml index 81148cc27b4a..99dceaecd154 100644 --- a/mmv1/products/chronicle/Rule.yaml +++ b/mmv1/products/chronicle/Rule.yaml @@ -58,6 +58,7 @@ samples: chronicle_id: 'CHRONICLE_ID' exclude_import_test: true custom_code: + constants: 'templates/terraform/constants/chronicle_rule.go.tmpl' pre_delete: 'templates/terraform/pre_delete/chronicle_rule.go.tmpl' virtual_fields: - name: 'deletion_policy' @@ -110,6 +111,7 @@ properties: Rule Id is the ID of the Rule. - name: text type: String + diff_suppress_func: 'chronicleRuleTextDiffSuppress' description: |- The YARA-L content of the rule. Populated in FULL view. diff --git a/mmv1/templates/terraform/constants/chronicle_rule.go.tmpl b/mmv1/templates/terraform/constants/chronicle_rule.go.tmpl new file mode 100644 index 000000000000..b3bd2c27cc4e --- /dev/null +++ b/mmv1/templates/terraform/constants/chronicle_rule.go.tmpl @@ -0,0 +1,8 @@ +// chronicleRuleTextDiffSuppress treats two YARA-L rule bodies as equal when +// they differ only by trailing newline characters. The Chronicle Rules API +// silently appends a trailing "\n" to every stored rule body, so a rule +// submitted as `}` is read back as `}\n`, which would otherwise show as a +// permanent diff on every plan (see hashicorp/terraform-provider-google#27881). +func chronicleRuleTextDiffSuppress(_, old, new string, _ *schema.ResourceData) bool { + return strings.TrimRight(old, "\n") == strings.TrimRight(new, "\n") +} From 94504b0481e6dfc37e13a0ee57522a3f8f5feca9 Mon Sep 17 00:00:00 2001 From: yliaog Date: Mon, 6 Jul 2026 16:44:00 -0700 Subject: [PATCH 41/78] added node pool maintenance policy (#17190) --- .../resource_container_cluster_meta.yaml.tmpl | 6 ++ .../resource_container_cluster_test.go.tmpl | 57 ++++++++++++++ .../resource_container_node_pool.go.tmpl | 75 +++++++++++++++++++ ...esource_container_node_pool_meta.yaml.tmpl | 3 + .../resource_container_node_pool_test.go.tmpl | 58 ++++++++++++++ .../docs/r/container_node_pool.html.markdown | 12 +++ 6 files changed, 211 insertions(+) diff --git a/mmv1/third_party/terraform/services/container/resource_container_cluster_meta.yaml.tmpl b/mmv1/third_party/terraform/services/container/resource_container_cluster_meta.yaml.tmpl index 03b02c6ee135..34c5513d8f8b 100644 --- a/mmv1/third_party/terraform/services/container/resource_container_cluster_meta.yaml.tmpl +++ b/mmv1/third_party/terraform/services/container/resource_container_cluster_meta.yaml.tmpl @@ -778,6 +778,12 @@ fields: api_field: 'nodePools.nodeDrainConfig.pdbTimeoutDuration' - field: 'node_pool.node_drain_config.respect_pdb_during_node_pool_deletion' api_field: 'nodePools.nodeDrainConfig.respectPdbDuringNodePoolDeletion' + - field: 'node_pool.maintenance_policy.exclusion_until_end_of_support.enabled' + api_field: 'nodePools.maintenancePolicy.exclusionUntilEndOfSupport.enabled' + - field: 'node_pool.maintenance_policy.exclusion_until_end_of_support.start_time' + api_field: 'nodePools.maintenancePolicy.exclusionUntilEndOfSupport.startTime' + - field: 'node_pool.maintenance_policy.exclusion_until_end_of_support.end_time' + api_field: 'nodePools.maintenancePolicy.exclusionUntilEndOfSupport.endTime' - field: 'node_pool.node_count' provider_only: true - field: 'node_pool.node_locations' diff --git a/mmv1/third_party/terraform/services/container/resource_container_cluster_test.go.tmpl b/mmv1/third_party/terraform/services/container/resource_container_cluster_test.go.tmpl index 02d53f9bce5a..b9300ee5a431 100644 --- a/mmv1/third_party/terraform/services/container/resource_container_cluster_test.go.tmpl +++ b/mmv1/third_party/terraform/services/container/resource_container_cluster_test.go.tmpl @@ -3851,6 +3851,36 @@ func TestAccContainerCluster_withNodePoolNodeDrainConfig(t *testing.T) { }) } +func TestAccContainerCluster_withNodePoolMaintenancePolicy(t *testing.T) { + t.Parallel() + + cluster := fmt.Sprintf("tf-test-cluster-%s", acctest.RandString(t, 10)) + np := fmt.Sprintf("tf-test-np-%s", acctest.RandString(t, 10)) + networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") + subnetworkName := tpgcompute.BootstrapSubnet(t, "gke-cluster", networkName) + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccCheckContainerClusterDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccContainerCluster_withNodePoolMaintenancyPolicy(cluster, np, networkName, subnetworkName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_container_cluster.with_node_pool_maintenance_policy", + "node_pool.0.maintenance_policy.0.exclusion_until_end_of_support.0.enabled", "true"), + ), + }, + { + ResourceName: "google_container_cluster.with_node_pool_maintenance_policy", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"min_master_version", "deletion_protection"}, + }, + }, + }) +} + func TestAccContainerCluster_withClusterDisruptionBudget(t *testing.T) { t.Parallel() @@ -11811,6 +11841,33 @@ resource "google_container_cluster" "with_node_pool_node_drain_config" { `, cluster, np, privateName, privateVal, networkName, subnetworkName) } +func testAccContainerCluster_withNodePoolMaintenancyPolicy(cluster, np, networkName, subnetworkName string) string { + return fmt.Sprintf(` +data "google_container_engine_versions" "central1a" { + location = "us-central1-a" +} + +resource "google_container_cluster" "with_node_pool_maintenance_policy" { + name = "%s" + location = "us-central1-a" + min_master_version = data.google_container_engine_versions.central1a.latest_master_version + node_pool { + name = "%s" + initial_node_count = 1 + maintenance_policy { + exclusion_until_end_of_support { + enabled = true + } + } + } + + network = "%s" + subnetwork = "%s" + deletion_protection = false +} +`, cluster, np, networkName, subnetworkName) +} + func testAccContainerCluster_withClusterDisruptionBudget(clusterName, interval, networkName, subnetworkName string) string { return fmt.Sprintf(` resource "google_container_cluster" "with_cluster_disruption_budget" { diff --git a/mmv1/third_party/terraform/services/container/resource_container_node_pool.go.tmpl b/mmv1/third_party/terraform/services/container/resource_container_node_pool.go.tmpl index 8b0c129fc2a9..b432f70529f3 100644 --- a/mmv1/third_party/terraform/services/container/resource_container_node_pool.go.tmpl +++ b/mmv1/third_party/terraform/services/container/resource_container_node_pool.go.tmpl @@ -706,6 +706,40 @@ var schemaNodePool = map[string]*schema.Schema{ Optional: true, Description: `When true, the provider ignores external changes (drift) to the node count by skipping GCE API queries to the Instance Group Managers. This is a performance optimization for large clusters that saves API quota. Setting this to true will result in missing managed_instance_group_urls in the state.`, }, + + "maintenance_policy": { + Type: schema.TypeList, + Optional: true, + Description: `Maintenance policy for this NodePool.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "exclusion_until_end_of_support": { + Type: schema.TypeList, + Optional: true, + Description: `Maintenance exclusion until the end of support.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Description: `Whether to enable the maintenance exclusion until the end of support for this NodePool.`, + }, + "start_time": { + Type: schema.TypeString, + Computed: true, + Description: `Start time of the maintenance exclusion.`, + }, + "end_time": { + Type: schema.TypeString, + Computed: true, + Description: `End time of the maintenance exclusion.`, + }, + }, + }, + }, + }, + }, + }, } type NodePoolInformation struct { @@ -1349,6 +1383,19 @@ func expandNodePool(d *schema.ResourceData, prefix string) (*container.NodePool, } } + if v, ok := d.GetOk(prefix + "maintenance_policy"); ok { + maintenancePolicy := v.([]interface{})[0].(map[string]interface{}) + if v, ok := maintenancePolicy["exclusion_until_end_of_support"]; ok && len(v.([]interface{})) > 0 { + np.MaintenancePolicy = &container.NodePoolMaintenancePolicy{ + ExclusionUntilEndOfSupport: &container.ExclusionUntilEndOfSupport {}, + } + exclusionUntilEndOfSupport := v.([]interface{})[0].(map[string]interface{}) + if v, ok := exclusionUntilEndOfSupport["enabled"]; ok { + np.MaintenancePolicy.ExclusionUntilEndOfSupport.Enabled = v.(bool) + } + } + } + return np, nil } @@ -1423,6 +1470,30 @@ func flattenNodePoolNodeDrainConfig(ndc *container.NodeDrainConfig) []map[string return []map[string]interface{}{nodeDrainConfig} } +func flattenExlusionUntilEndOfSupport(ex *container.ExclusionUntilEndOfSupport) []map[string]interface{} { + if ex == nil { + return nil + } + return []map[string]interface{}{ + { + "enabled": ex.Enabled, + "start_time": ex.StartTime, + "end_time": ex.EndTime, + }, + } +} + +func flattenNodePoolMaintenancePolicy(mp *container.NodePoolMaintenancePolicy) []map[string]interface{} { + if mp == nil { + return nil + } + + maintenancePolicy := make(map[string]interface{}) + + maintenancePolicy["exclusion_until_end_of_support"] = flattenExlusionUntilEndOfSupport(mp.ExclusionUntilEndOfSupport) + return []map[string]interface{}{maintenancePolicy} +} + func flattenNodePool(d *schema.ResourceData, config *transport_tpg.Config, np *container.NodePool, prefix string) (map[string]interface{}, error) { userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) if err != nil { @@ -1551,6 +1622,10 @@ func flattenNodePool(d *schema.ResourceData, config *transport_tpg.Config, np *c nodePool["node_drain_config"] = flattenNodePoolNodeDrainConfig(np.NodeDrainConfig) } + if np.MaintenancePolicy != nil { + nodePool["maintenance_policy"] = flattenNodePoolMaintenancePolicy(np.MaintenancePolicy) + } + return nodePool, nil } diff --git a/mmv1/third_party/terraform/services/container/resource_container_node_pool_meta.yaml.tmpl b/mmv1/third_party/terraform/services/container/resource_container_node_pool_meta.yaml.tmpl index 6276135c40aa..d3022818ce32 100644 --- a/mmv1/third_party/terraform/services/container/resource_container_node_pool_meta.yaml.tmpl +++ b/mmv1/third_party/terraform/services/container/resource_container_node_pool_meta.yaml.tmpl @@ -329,6 +329,9 @@ fields: - api_field: 'nodeDrainConfig.graceTerminationDuration' - api_field: 'nodeDrainConfig.pdbTimeoutDuration' - api_field: 'nodeDrainConfig.respectPdbDuringNodePoolDeletion' + - api_field: 'maintenancePolicy.exclusionUntilEndOfSupport.enabled' + - api_field: 'maintenancePolicy.exclusionUntilEndOfSupport.startTime' + - api_field: 'maintenancePolicy.exclusionUntilEndOfSupport.endTime' - field: 'node_count' provider_only: true - field: 'node_locations' diff --git a/mmv1/third_party/terraform/services/container/resource_container_node_pool_test.go.tmpl b/mmv1/third_party/terraform/services/container/resource_container_node_pool_test.go.tmpl index b19067fe41af..008679561fd8 100644 --- a/mmv1/third_party/terraform/services/container/resource_container_node_pool_test.go.tmpl +++ b/mmv1/third_party/terraform/services/container/resource_container_node_pool_test.go.tmpl @@ -1758,6 +1758,34 @@ func TestAccContainerNodePool_withNodeDrainConfig(t *testing.T) { }) } +func TestAccContainerNodePool_withMaintenancePolicy(t *testing.T) { + t.Parallel() + + cluster := fmt.Sprintf("tf-test-cluster-%s", acctest.RandString(t, 10)) + nodePool := fmt.Sprintf("tf-test-nodepool-%s", acctest.RandString(t, 10)) + networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") + subnetworkName := tpgcompute.BootstrapSubnet(t, "gke-cluster", networkName) + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccCheckContainerNodePoolDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccContainerNodePool_withMaintenancePolicy(cluster, nodePool, networkName, subnetworkName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_container_node_pool.np_with_maintenance_policy", "maintenance_policy.0.exclusion_until_end_of_support.0.enabled", "true"), + ), + }, + { + ResourceName: "google_container_node_pool.np_with_maintenance_policy", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + func TestAccContainerNodePool_withAccurateTimeConfig(t *testing.T) { t.Parallel() @@ -5087,6 +5115,36 @@ resource "google_container_node_pool" "np_with_node_drain_config" { `, cluster, networkName, subnetworkName, np, privateName, privateVal) } +func testAccContainerNodePool_withMaintenancePolicy(cluster, np, networkName, subnetworkName string) string { + return fmt.Sprintf(` +data "google_container_engine_versions" "central1a" { + location = "us-central1-a" +} + +resource "google_container_cluster" "cluster" { + name = "%s" + location = "us-central1-a" + initial_node_count = 1 + min_master_version = data.google_container_engine_versions.central1a.latest_master_version + deletion_protection = false + network = "%s" + subnetwork = "%s" +} + +resource "google_container_node_pool" "np_with_maintenance_policy" { + name = "%s" + location = "us-central1-a" + cluster = google_container_cluster.cluster.name + initial_node_count = 1 + maintenance_policy { + exclusion_until_end_of_support { + enabled = true + } + } +} +`, cluster, networkName, subnetworkName, np) +} + func testAccContainerNodePool_withAccurateTimeConfig(cluster, np, networkName, subnetworkName string, enablePTP bool) string { return fmt.Sprintf(` data "google_container_engine_versions" "central1a" { diff --git a/mmv1/third_party/terraform/website/docs/r/container_node_pool.html.markdown b/mmv1/third_party/terraform/website/docs/r/container_node_pool.html.markdown index 363ec15c6c49..d612e187ce81 100644 --- a/mmv1/third_party/terraform/website/docs/r/container_node_pool.html.markdown +++ b/mmv1/third_party/terraform/website/docs/r/container_node_pool.html.markdown @@ -159,6 +159,8 @@ cluster. * `node_drain_config` - (Optional) The node drain configuration of the pool. Structure is [documented below](#nested_node_drain_config). +* `maintenance_policy` - (Optional) The maintenance policy of the pool. Structure is [documented below](#nested_maintenance_policy). + * `project` - (Optional) The ID of the project in which to create the node pool. If blank, the provider-configured project will be used. @@ -267,6 +269,16 @@ cluster. * `respect_pdb_during_node_pool_deletion` - (Optional) Whether to respect PodDisruptionBudget policy during node pool deletion. +The `maintenance_policy` block supports: + +* `exclusion_until_end_of_support` - (Optional) When enabled, the node pool will not be automatically upgraded by GKE until the node pool version's end of support date. Structure is [documented below](#nested_exclusion_until_end_of_support). + +The `exclusion_until_end_of_support` block supports: + +* `enabled` - (Optional) When true, the node pool will not be automatically upgraded by GKE until the node pool version's end of support date. +* `start_time` - (Optional) The time when the maintenance policy is first created. +* `end_time` - (Optional) The time when the maintenance policy is no longer effective, i.e., the node pool version's end of support date. + The `upgrade_settings` block supports: * `max_surge` - (Optional) The number of additional nodes that can be added to the node pool during From d038252cf43152b1f52709b68676d799879ce403 Mon Sep 17 00:00:00 2001 From: Shengquan Ni <13672781+shengquan-ni@users.noreply.github.com> Date: Tue, 7 Jul 2026 10:31:48 -0700 Subject: [PATCH 42/78] Add Google Cloud Network Connectivity CustomHardwareInstance resource (#17741) --- .../CustomHardwareInstance.yaml | 85 +++++++++++++++++++ ...ity_custom_hardware_instance_basic.tf.tmpl | 8 ++ 2 files changed, 93 insertions(+) create mode 100755 mmv1/products/networkconnectivity/CustomHardwareInstance.yaml create mode 100644 mmv1/templates/terraform/samples/services/networkconnectivity/network_connectivity_custom_hardware_instance_basic.tf.tmpl diff --git a/mmv1/products/networkconnectivity/CustomHardwareInstance.yaml b/mmv1/products/networkconnectivity/CustomHardwareInstance.yaml new file mode 100755 index 000000000000..649b3439b1b5 --- /dev/null +++ b/mmv1/products/networkconnectivity/CustomHardwareInstance.yaml @@ -0,0 +1,85 @@ +# 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: CustomHardwareInstance +description: Description +base_url: projects/{{project}}/locations/{{location}}/customHardwareInstances +self_link: projects/{{project}}/locations/{{location}}/customHardwareInstances/{{custom_hardware_instance_id}} +create_url: projects/{{project}}/locations/{{location}}/customHardwareInstances?customHardwareInstanceId={{custom_hardware_instance_id}} +update_mask: true +update_verb: PATCH +import_format: + - projects/{{project}}/locations/{{location}}/customHardwareInstances/{{custom_hardware_instance_id}} +samples: + - name: network_connectivity_custom_hardware_instance_basic + primary_resource_id: instance + min_version: beta + steps: + - name: network_connectivity_custom_hardware_instance_basic + resource_id_vars: + resource_name: ch-test-instance + vars: + env_label: "test" + - name: network_connectivity_custom_hardware_instance_basic + resource_id_vars: + resource_name: ch-test-instance + vars: + env_label: "prod" +async: + operation: + base_url: '{{op_id}}' + actions: + - create + - update + - delete + result: + resource_inside_response: true +autogen_status: Q3VzdG9tSGFyZHdhcmVJbnN0YW5jZQ== +autogen_async: true +parameters: + - name: location + type: String + required: true + 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 + - name: customHardwareInstanceId + type: String + required: true + description: |- + Unique id of the CustomHardwareInstance. + This is restricted to letters, numbers and with the first + character being a letter, the last a letter or a number. Matching the regex + `^[a-zA-Z0-9-]*[a-zA-Z0-9]$`. + immutable: true + url_param_only: true +properties: + - name: createTime + type: String + description: Time when the CustomHardwareInstance was created. + output: true + - name: labels + type: KeyValueLabels + description: User-defined labels. + - name: name + type: String + description: |- + Identifier. The name of a CustomHardwareInstance. + This is populated by the service. + output: true + - name: updateTime + type: String + description: Time when the CustomHardwareInstance was updated. + output: true +min_version: beta diff --git a/mmv1/templates/terraform/samples/services/networkconnectivity/network_connectivity_custom_hardware_instance_basic.tf.tmpl b/mmv1/templates/terraform/samples/services/networkconnectivity/network_connectivity_custom_hardware_instance_basic.tf.tmpl new file mode 100644 index 000000000000..fc8196a739ad --- /dev/null +++ b/mmv1/templates/terraform/samples/services/networkconnectivity/network_connectivity_custom_hardware_instance_basic.tf.tmpl @@ -0,0 +1,8 @@ +resource "google_network_connectivity_custom_hardware_instance" "instance" { + provider = google-beta + custom_hardware_instance_id = "{{index $.ResourceIdVars "resource_name"}}" + location = "us-south1" + labels = { + env = "{{index $.Vars "env_label"}}" + } +} From 3a06d8605d56601c8f6e9f4ca5d2988e26c77693 Mon Sep 17 00:00:00 2001 From: Chris Hawk Date: Tue, 7 Jul 2026 10:42:53 -0700 Subject: [PATCH 43/78] Fallback to async timeouts if top level timeouts are not specified (#18190) --- mmv1/api/resource.go | 2 +- mmv1/products/firebaseapphosting/Build.yaml | 2 ++ mmv1/products/firebaseapphosting/DefaultDomain.yaml | 1 + mmv1/products/firebaseapphosting/Domain.yaml | 1 + mmv1/products/observability/FolderSettings.yaml | 1 + mmv1/products/observability/OrganizationSettings.yaml | 1 + mmv1/products/observability/ProjectSettings.yaml | 1 + mmv1/templates/terraform/resource.go.tmpl | 6 +++--- mmv1/templates/terraform/resource.html.markdown.tmpl | 6 +++--- mmv1/templates/terraform/resource_fw.go.tmpl | 6 +++--- 10 files changed, 17 insertions(+), 10 deletions(-) diff --git a/mmv1/api/resource.go b/mmv1/api/resource.go index 201cda61f74d..2bf5800e48b7 100644 --- a/mmv1/api/resource.go +++ b/mmv1/api/resource.go @@ -1337,7 +1337,7 @@ func (r Resource) PackageName() string { // general defined timeouts, or default Timeouts func (r Resource) GetTimeouts() *Timeouts { timeoutsFiltered := r.Timeouts - if timeoutsFiltered == nil { + if timeoutsFiltered == nil || timeoutsFiltered.IsZero() { if async := r.GetAsync(); async != nil && async.Operation != nil { timeoutsFiltered = async.Operation.Timeouts } diff --git a/mmv1/products/firebaseapphosting/Build.yaml b/mmv1/products/firebaseapphosting/Build.yaml index 9034fe546c1e..17c18f9cde24 100644 --- a/mmv1/products/firebaseapphosting/Build.yaml +++ b/mmv1/products/firebaseapphosting/Build.yaml @@ -33,6 +33,8 @@ async: operation: timeouts: insert_minutes: 20 + update_minutes: 20 + delete_minutes: 20 base_url: '{{op_id}}' actions: - create diff --git a/mmv1/products/firebaseapphosting/DefaultDomain.yaml b/mmv1/products/firebaseapphosting/DefaultDomain.yaml index 197cee9c9541..5065dc4864a8 100644 --- a/mmv1/products/firebaseapphosting/DefaultDomain.yaml +++ b/mmv1/products/firebaseapphosting/DefaultDomain.yaml @@ -32,6 +32,7 @@ async: timeouts: insert_minutes: 20 update_minutes: 20 + delete_minutes: 20 base_url: '{{op_id}}' actions: - create diff --git a/mmv1/products/firebaseapphosting/Domain.yaml b/mmv1/products/firebaseapphosting/Domain.yaml index f24292d5866d..69e691ce5c5f 100644 --- a/mmv1/products/firebaseapphosting/Domain.yaml +++ b/mmv1/products/firebaseapphosting/Domain.yaml @@ -28,6 +28,7 @@ async: type: PollAsync operation: timeouts: + insert_minutes: 20 update_minutes: 20 delete_minutes: 20 base_url: '{{op_id}}' diff --git a/mmv1/products/observability/FolderSettings.yaml b/mmv1/products/observability/FolderSettings.yaml index ef861a149d4a..0720606544b5 100644 --- a/mmv1/products/observability/FolderSettings.yaml +++ b/mmv1/products/observability/FolderSettings.yaml @@ -29,6 +29,7 @@ async: timeouts: insert_minutes: 10 update_minutes: 10 + delete_minutes: 20 base_url: '{{op_id}}' result: resource_inside_response: true diff --git a/mmv1/products/observability/OrganizationSettings.yaml b/mmv1/products/observability/OrganizationSettings.yaml index 2390db7fd317..61749d7e1fe4 100644 --- a/mmv1/products/observability/OrganizationSettings.yaml +++ b/mmv1/products/observability/OrganizationSettings.yaml @@ -29,6 +29,7 @@ async: timeouts: insert_minutes: 10 update_minutes: 10 + delete_minutes: 20 base_url: '{{op_id}}' result: resource_inside_response: true diff --git a/mmv1/products/observability/ProjectSettings.yaml b/mmv1/products/observability/ProjectSettings.yaml index 7371c3c0d7c8..f57a8f4cbd64 100644 --- a/mmv1/products/observability/ProjectSettings.yaml +++ b/mmv1/products/observability/ProjectSettings.yaml @@ -29,6 +29,7 @@ async: timeouts: insert_minutes: 10 update_minutes: 10 + delete_minutes: 20 base_url: '{{op_id}}' result: resource_inside_response: true diff --git a/mmv1/templates/terraform/resource.go.tmpl b/mmv1/templates/terraform/resource.go.tmpl index 1221acd08e0f..d38c9681e78c 100644 --- a/mmv1/templates/terraform/resource.go.tmpl +++ b/mmv1/templates/terraform/resource.go.tmpl @@ -119,11 +119,11 @@ func Resource{{ $.ResourceName -}}() *schema.Resource { {{- end}} Timeouts: &schema.ResourceTimeout { - Create: schema.DefaultTimeout({{ $.Timeouts.InsertMinutes -}} * time.Minute), + Create: schema.DefaultTimeout({{ $.GetTimeouts.InsertMinutes -}} * time.Minute), {{- if or (or $.Updatable $.RootLabels) $.VirtualFields }} - Update: schema.DefaultTimeout({{ $.Timeouts.UpdateMinutes -}} * time.Minute), + Update: schema.DefaultTimeout({{ $.GetTimeouts.UpdateMinutes -}} * time.Minute), {{- end}} - Delete: schema.DefaultTimeout({{ $.Timeouts.DeleteMinutes -}} * time.Minute), + Delete: schema.DefaultTimeout({{ $.GetTimeouts.DeleteMinutes -}} * time.Minute), }, {{ if $.SchemaVersion }} SchemaVersion: {{ $.SchemaVersion -}}, diff --git a/mmv1/templates/terraform/resource.html.markdown.tmpl b/mmv1/templates/terraform/resource.html.markdown.tmpl index 318f4cdc2ab0..4a02370957c5 100644 --- a/mmv1/templates/terraform/resource.html.markdown.tmpl +++ b/mmv1/templates/terraform/resource.html.markdown.tmpl @@ -161,11 +161,11 @@ In addition to the arguments listed above, the following computed attributes are This resource provides the following [Timeouts](https://developer.hashicorp.com/terraform/plugin/sdkv2/resources/retries-and-customizable-timeouts) configuration options: -- `create` - Default is {{$.Timeouts.InsertMinutes}} minutes. +- `create` - Default is {{$.GetTimeouts.InsertMinutes}} minutes. {{- if or $.Updatable $.RootLabels }} -- `update` - Default is {{$.Timeouts.UpdateMinutes}} minutes. +- `update` - Default is {{$.GetTimeouts.UpdateMinutes}} minutes. {{- end }} -- `delete` - Default is {{$.Timeouts.DeleteMinutes}} minutes. +- `delete` - Default is {{$.GetTimeouts.DeleteMinutes}} minutes. {{ if $.ProductMetadata.Version.RepEnabled }} ## Regional Endpoint Policies diff --git a/mmv1/templates/terraform/resource_fw.go.tmpl b/mmv1/templates/terraform/resource_fw.go.tmpl index e24c43f4d5f0..9841feccbf0c 100644 --- a/mmv1/templates/terraform/resource_fw.go.tmpl +++ b/mmv1/templates/terraform/resource_fw.go.tmpl @@ -270,7 +270,7 @@ func (r *{{$.ResourceName}}FWResource) Create(ctx context.Context, req resource. obj["{{ $prop.ApiName -}}"] = {{ $prop.ApiName -}}Prop {{- end }} - createTimeout, diags := data.Timeouts.Create(ctx, {{ $.Timeouts.InsertMinutes }}*time.Minute) + createTimeout, diags := data.Timeouts.Create(ctx, {{ $.GetTimeouts.InsertMinutes }}*time.Minute) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return @@ -487,7 +487,7 @@ func (r *{{$.ResourceName}}FWResource) Update(ctx context.Context, req resource. } {{- end }} - updateTimeout, diags := plan.Timeouts.Update(ctx, {{ $.Timeouts.UpdateMinutes }}*time.Minute) + updateTimeout, diags := plan.Timeouts.Update(ctx, {{ $.GetTimeouts.UpdateMinutes }}*time.Minute) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return @@ -629,7 +629,7 @@ func (r *{{$.ResourceName}}FWResource) Delete(ctx context.Context, req resource. obj := make(map[string]interface{}) - deleteTimeout, diags := data.Timeouts.Delete(ctx, {{ $.Timeouts.DeleteMinutes }}*time.Minute) + deleteTimeout, diags := data.Timeouts.Delete(ctx, {{ $.GetTimeouts.DeleteMinutes }}*time.Minute) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return From 4117ded8b8efd5b6030289dded8c04f6aa7b3f36 Mon Sep 17 00:00:00 2001 From: "Stephen Lewis (Burrows)" Date: Tue, 7 Jul 2026 10:53:18 -0700 Subject: [PATCH 44/78] Update enrolled_teams.yml (#18208) --- .../issue-labeler/labeler/enrolled_teams.yml | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/tools/issue-labeler/labeler/enrolled_teams.yml b/tools/issue-labeler/labeler/enrolled_teams.yml index d7cca74aa659..ad97f6873900 100755 --- a/tools/issue-labeler/labeler/enrolled_teams.yml +++ b/tools/issue-labeler/labeler/enrolled_teams.yml @@ -7,6 +7,12 @@ service/accessapproval: service/accesscontextmanager: resources: - google_access_context_manager_.* +service/agentidentity: + resources: + - google_agent_identity_.* +service/agentregistry: + resources: + - google_agent_registry_.* service/aiplatform-agent-engine: resources: - google_vertex_ai_reasoning_engine.* @@ -27,6 +33,7 @@ service/aiplatform-matching-engine: service/aiplatform-metadata: resources: - google_vertex_ai_metadata_store + - google_vertex_ai_schedule service/aiplatform-prediction: resources: - google_vertex_ai_cache_config @@ -34,6 +41,7 @@ service/aiplatform-prediction: - google_vertex_ai_deployment_resource_pool - google_ml_engine_model - google_vertex_ai_endpoint_with_model_garden_deployment + - google_vertex_ai_model_garden_enable_model service/aiplatform-rag-engine: resources: - google_vertex_ai_rag_engine_config @@ -42,7 +50,7 @@ service/aiplatform-semantic-governance: - google_vertex_ai_semantic_governance.* service/aiplatform-tensorboard: resources: - - google_vertex_ai_tensorboard + - google_vertex_ai_tensorboard.* service/alloydb: resources: - google_alloydb_.* @@ -122,7 +130,6 @@ service/cloudbilling: service/cloudbuild: resources: - google_cloudbuild_.* - - google_cloudbuildv2_.* service/clouddeploy: resources: - google_clouddeploy_.* @@ -248,6 +255,7 @@ service/compute-managed: - google_compute_region_instance_group_manager.* - google_compute_per_instance_config.* - google_compute_region_per_instance_config.* + - google_compute_bulk_per_instance_config.* - google_compute_resize_request.* - google_compute_region_resize_request.* service/compute-nat: @@ -380,9 +388,11 @@ service/deploymentmanager: service/developerconnect: resources: - google_developer_connect_.* + - google_cloudbuildv2_.* service/dialogflow: resources: - google_dialogflow_conversation_profile + - google_dialogflow_sip_trunk service/dialogflow-cx: resources: - google_dialogflow_cx_.* @@ -498,6 +508,7 @@ service/iam-core: - google_iam_role - google_iam_.*_policy_binding - google_iam_principal_access_boundary_policy + - google_iam_.*_access_policy - google_iam_testable_permissions service/iam-serviceaccount: resources: @@ -529,6 +540,9 @@ service/integrationconnectors: service/integrations: resources: - google_integrations_.* +service/licensemanager: + resources: + - google_license_manager_configuration service/logging: resources: - google_logging_.* @@ -607,6 +621,9 @@ service/network-security-distributed-firewall: - google_compute_firewall.* - google_compute_network_firewall_policy.* - google_compute_region_network_firewall_policy.* +service/networkconnectivity-customhardware: + resources: + - google_network_connectivity_custom_hardware_.* service/networkconnectivity-private-range: resources: - google_network_connectivity_internal_range @@ -638,6 +655,9 @@ service/networksecurity-swp: - google_network_security_gateway_security_policy_rule - google_network_security_url_lists - google_network_security_tls_inspection_policy +service/networkservices-agent-gateway: + resources: + - google_network_services_agent_gateway service/networkservices-media-cdn: resources: - google_network_services_edge_cache_.* @@ -674,6 +694,7 @@ service/orgpolicy: service/osconfig: resources: - google_os_config_.* + - google_compute_.*_vm_extension_policy service/oslogin: resources: - google_os_login_.* From 8ffc043d806300d67eb1ee89a59c31dc5c717e87 Mon Sep 17 00:00:00 2001 From: James Duncan Date: Tue, 7 Jul 2026 14:11:39 -0400 Subject: [PATCH 45/78] Add IAM policy support for google_iap_agent_registry_endpoint (#18201) Co-authored-by: James Duncan --- mmv1/products/iap/AgentRegistryEndpoint.yaml | 58 +++++++++++++++++++ .../iap_agent_registry_endpoint.tf.tmpl | 3 + .../iap/iap_existing_project_endpoint.tf.tmpl | 15 +++++ 3 files changed, 76 insertions(+) create mode 100644 mmv1/products/iap/AgentRegistryEndpoint.yaml create mode 100644 mmv1/templates/terraform/iam/example_config_body/iap_agent_registry_endpoint.tf.tmpl create mode 100644 mmv1/templates/terraform/samples/services/iap/iap_existing_project_endpoint.tf.tmpl diff --git a/mmv1/products/iap/AgentRegistryEndpoint.yaml b/mmv1/products/iap/AgentRegistryEndpoint.yaml new file mode 100644 index 000000000000..4b50cea93766 --- /dev/null +++ b/mmv1/products/iap/AgentRegistryEndpoint.yaml @@ -0,0 +1,58 @@ +--- +# 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: AgentRegistryEndpoint +description: | + Only used to generate IAM resources +docs: +base_url: projects/{{project}}/locations/{{location}}/iap_web/agentRegistry/endpoints/{{endpointId}} +self_link: projects/{{project}}/locations/{{location}}/iap_web/agentRegistry/endpoints/{{endpointId}} +iam_policy: + method_name_separator: ':' + parent_resource_type: google_project_service + parent_is_resource_id: true + fetch_iam_policy_verb: POST + allowed_iam_role: roles/iap.egressor + parent_resource_attribute: endpoint_id + sample_config_body: templates/terraform/iam/example_config_body/iap_agent_registry_endpoint.tf.tmpl + iam_conditions_request_type: REQUEST_BODY +# This resource is only used to generate IAM resources. They do not correspond to real +# GCP resources, and should not be used to generate anything other than IAM support. +exclude_resource: true +id_format: projects/{{project}}/locations/{{location}}/iap_web/agentRegistry/endpoints/{{endpointId}} +import_format: + - projects/{{project}}/locations/{{location}}/iap_web/agentRegistry/endpoints/{{endpointId}} + - '{{endpointId}}' +timeouts: + insert_minutes: 20 + update_minutes: 20 + delete_minutes: 20 +exclude_tgc: true +samples: + - name: project + primary_resource_id: project_service + steps: + - name: project + config_path: templates/terraform/samples/services/iap/iap_existing_project_endpoint.tf.tmpl + test_env_vars: + project: PROJECT_NAME +parameters: + - name: location + type: String + required: true + description: The location of the resource. +properties: + - name: endpointId + type: String + description: The ID of the Endpoint. + required: true diff --git a/mmv1/templates/terraform/iam/example_config_body/iap_agent_registry_endpoint.tf.tmpl b/mmv1/templates/terraform/iam/example_config_body/iap_agent_registry_endpoint.tf.tmpl new file mode 100644 index 000000000000..61b6606c107a --- /dev/null +++ b/mmv1/templates/terraform/iam/example_config_body/iap_agent_registry_endpoint.tf.tmpl @@ -0,0 +1,3 @@ + project = "%{project}" + location = "us-central1" + endpoint_id = basename(google_agent_registry_service.default.registry_resource) diff --git a/mmv1/templates/terraform/samples/services/iap/iap_existing_project_endpoint.tf.tmpl b/mmv1/templates/terraform/samples/services/iap/iap_existing_project_endpoint.tf.tmpl new file mode 100644 index 000000000000..9d7bcfbc9366 --- /dev/null +++ b/mmv1/templates/terraform/samples/services/iap/iap_existing_project_endpoint.tf.tmpl @@ -0,0 +1,15 @@ +resource "google_agent_registry_service" "default" { + location = "us-central1" + service_id = "tf-test-endpoint-%{random_suffix}" + description = "My endpoint agent registry service for IAM test" + display_name = "My Service" + + interfaces { + url = "https://tf-test-endpoint-%{random_suffix}.example.com" + protocol_binding = "HTTP_JSON" # Wait, what protocol bindings are allowed? + } + + endpoint_spec { + type = "NO_SPEC" + } +} From a0551a792a97e22a860b31f5c638c473d26a2ff6 Mon Sep 17 00:00:00 2001 From: Dennis Kugelmann Date: Tue, 7 Jul 2026 18:38:18 +0000 Subject: [PATCH 46/78] feat(firestore): Support for skip wait on Firestore field resource (#18050) --- mmv1/products/firestore/Field.yaml | 21 +++ .../custom_create/firestore_field.go.tmpl | 120 ++++++++++++++++++ .../custom_update/firestore_field.go.tmpl | 102 +++++++++++++++ .../firestore_field_skip_wait.tf.tmpl | 28 ++++ 4 files changed, 271 insertions(+) create mode 100644 mmv1/templates/terraform/custom_create/firestore_field.go.tmpl create mode 100644 mmv1/templates/terraform/custom_update/firestore_field.go.tmpl create mode 100644 mmv1/templates/terraform/samples/services/firestore/firestore_field_skip_wait.tf.tmpl diff --git a/mmv1/products/firestore/Field.yaml b/mmv1/products/firestore/Field.yaml index ddd7d402195d..92caaaaededd 100644 --- a/mmv1/products/firestore/Field.yaml +++ b/mmv1/products/firestore/Field.yaml @@ -53,6 +53,8 @@ error_retry_predicates: - transport_tpg.FirestoreField409RetryUnderlyingDataChanged autogen_async: true custom_code: + custom_create: templates/terraform/custom_create/firestore_field.go.tmpl + custom_update: templates/terraform/custom_update/firestore_field.go.tmpl encoder: templates/terraform/encoders/firestore_field.go.tmpl custom_delete: templates/terraform/custom_delete/firestore_field_delete.go.tmpl custom_import: templates/terraform/custom_import/firestore_field.go.tmpl @@ -135,6 +137,25 @@ samples: project_id: PROJECT_NAME test_vars_overrides: delete_protection_state: '"DELETE_PROTECTION_DISABLED"' + - name: firestore_field_skip_wait + primary_resource_id: skip_wait + steps: + - name: firestore_field_skip_wait + ignore_read_extra: + - skip_wait + resource_id_vars: + database_id: database-id + delete_protection_state: DELETE_PROTECTION_ENABLED + test_env_vars: + project_id: PROJECT_NAME + test_vars_overrides: + delete_protection_state: '"DELETE_PROTECTION_DISABLED"' +virtual_fields: + - name: skip_wait + type: Boolean + default_value: false + description: Whether to skip waiting for the field operation to complete. + ignore_read: true parameters: properties: - name: database diff --git a/mmv1/templates/terraform/custom_create/firestore_field.go.tmpl b/mmv1/templates/terraform/custom_create/firestore_field.go.tmpl new file mode 100644 index 000000000000..1bae4e1cb699 --- /dev/null +++ b/mmv1/templates/terraform/custom_create/firestore_field.go.tmpl @@ -0,0 +1,120 @@ +userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) +if err != nil { + return err +} + +obj := make(map[string]interface{}) +indexConfigProp, err := expandFirestoreFieldIndexConfig(d.Get("index_config"), d, config) +if err != nil { + return err +} else if v, ok := d.GetOkExists("index_config"); ok || !reflect.DeepEqual(v, indexConfigProp) { + obj["indexConfig"] = indexConfigProp +} +ttlConfigProp, err := expandFirestoreFieldTtlConfig(d.Get("ttl_config"), d, config) +if err != nil { + return err +} else if v, ok := d.GetOkExists("ttl_config"); ok || !reflect.DeepEqual(v, ttlConfigProp) { + obj["ttlConfig"] = ttlConfigProp +} + +obj, err = resourceFirestoreFieldEncoder(d, meta, obj) +if err != nil { + return err +} + +url, err := tpgresource.ReplaceVars(d, config, transport_tpg.BaseUrl(Product, config)+"projects/{{"{{"}}project{{"}}"}}/databases/{{"{{"}}database{{"}}"}}/collectionGroups/{{"{{"}}collection{{"}}"}}/fields/{{"{{"}}field{{"}}"}}") +if err != nil { + return err +} + +log.Printf("[DEBUG] Creating new Field: %#v", obj) +billingProject := "" + +project, err := tpgresource.GetProject(d, config) +if err != nil { + return fmt.Errorf("Error fetching project for Field: %s", err) +} +billingProject = project + +// err == nil indicates that the billing_project value was found +if bp, err := tpgresource.GetBillingProject(d, config); err == nil { + billingProject = bp +} + +headers := make(http.Header) +res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "PATCH", + Project: billingProject, + RawURL: url, + UserAgent: userAgent, + Body: obj, + Timeout: d.Timeout(schema.TimeoutCreate), + Headers: headers, + ErrorRetryPredicates: []transport_tpg.RetryErrorPredicateFunc{transport_tpg.FirestoreField409RetryUnderlyingDataChanged}, +}) +if err != nil { + return fmt.Errorf("Error creating Field: %s", err) +} + +// Store the ID now +id, err := tpgresource.ReplaceVars(d, config, "{{"{{"}}name{{"}}"}}") +if err != nil { + return fmt.Errorf("Error constructing id: %s", err) +} +d.SetId(id) + +if d.Get("skip_wait").(bool) { + // If skip_wait, the LRO will not be complete so the response will not be populated. + // Extract the field name from the metadata field. + metadata, ok := res["metadata"].(map[string]interface{}) + if !ok { + return fmt.Errorf("Error constructing id from result.") + } + field, ok := metadata["field"].(string) + if !ok { + return fmt.Errorf("Error constructing id from result.") + } + if err := d.Set("name", flattenFirestoreFieldName(field, d, config)); err != nil { + return err + } +} else { + // Use the resource in the operation response to populate + // identity fields and d.Id() before read + var opRes map[string]interface{} + err = FirestoreOperationWaitTimeWithResponse( + config, res, &opRes, project, "Creating Field", userAgent, + d.Timeout(schema.TimeoutCreate)) + if err != nil { + // The resource didn't actually create + d.SetId("") + + return fmt.Errorf("Error waiting to create Field: %s", err) + } + + if err := d.Set("name", flattenFirestoreFieldName(opRes["name"], d, config)); err != nil { + return err + } +} + +// This may have caused the ID to update - update it if so. +id, err = tpgresource.ReplaceVars(d, config, "{{"{{"}}name{{"}}"}}") +if err != nil { + return fmt.Errorf("Error constructing id: %s", err) +} +d.SetId(id) + +log.Printf("[DEBUG] Finished creating Field %q: %#v", d.Id(), res) + +identity, err := d.Identity() +if err == nil && identity != nil { + if nameValue, ok := d.GetOk("name"); ok && nameValue.(string) != "" { + if err = identity.Set("name", nameValue.(string)); err != nil { + return fmt.Errorf("Error setting name: %s", err) + } + } +} else { + log.Printf("[DEBUG] (Create) identity not set: %s", err) +} + +return resourceFirestoreFieldRead(d, meta) diff --git a/mmv1/templates/terraform/custom_update/firestore_field.go.tmpl b/mmv1/templates/terraform/custom_update/firestore_field.go.tmpl new file mode 100644 index 000000000000..e3a37a82a114 --- /dev/null +++ b/mmv1/templates/terraform/custom_update/firestore_field.go.tmpl @@ -0,0 +1,102 @@ +userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) +if err != nil { + return err +} +identity, err := d.Identity() +if err == nil && identity != nil { + if nameValue, ok := d.GetOk("name"); ok && nameValue.(string) != "" { + if err = identity.Set("name", nameValue.(string)); err != nil { + return fmt.Errorf("Error setting name: %s", err) + } + } +} else { + log.Printf("[DEBUG] (Update) identity not set: %s", err) +} + +billingProject := "" + +project, err := tpgresource.GetProject(d, config) +if err != nil { + return fmt.Errorf("Error fetching project for Field: %s", err) +} +billingProject = project + +obj := make(map[string]interface{}) +indexConfigProp, err := expandFirestoreFieldIndexConfig(d.Get("index_config"), d, config) +if err != nil { + return err +} else if v, ok := d.GetOkExists("index_config"); ok || !reflect.DeepEqual(v, indexConfigProp) { + obj["indexConfig"] = indexConfigProp +} +ttlConfigProp, err := expandFirestoreFieldTtlConfig(d.Get("ttl_config"), d, config) +if err != nil { + return err +} else if v, ok := d.GetOkExists("ttl_config"); ok || !reflect.DeepEqual(v, ttlConfigProp) { + obj["ttlConfig"] = ttlConfigProp +} + +obj, err = resourceFirestoreFieldEncoder(d, meta, obj) +if err != nil { + return err +} + +url, err := tpgresource.ReplaceVars(d, config, transport_tpg.BaseUrl(Product, config)+"{{"{{"}}name{{"}}"}}") +if err != nil { + return err +} + +log.Printf("[DEBUG] Updating Field %q: %#v", d.Id(), obj) +headers := make(http.Header) +updateMask := []string{} + +if d.HasChange("index_config") { + updateMask = append(updateMask, "indexConfig") +} + +if d.HasChange("ttl_config") { + updateMask = append(updateMask, "ttlConfig") +} +// updateMask is a URL parameter but not present in the schema, so ReplaceVars +// won't set it +url, err = transport_tpg.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 := tpgresource.GetBillingProject(d, config); err == nil { + billingProject = bp +} + +// if updateMask is empty we are not updating anything so skip the post +if len(updateMask) > 0 { + res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "PATCH", + Project: billingProject, + RawURL: url, + UserAgent: userAgent, + Body: obj, + Timeout: d.Timeout(schema.TimeoutUpdate), + Headers: headers, + ErrorRetryPredicates: []transport_tpg.RetryErrorPredicateFunc{transport_tpg.FirestoreField409RetryUnderlyingDataChanged}, + }) + + if err != nil { + return fmt.Errorf("Error updating Field %q: %s", d.Id(), err) + } else { + log.Printf("[DEBUG] Finished updating Field %q: %#v", d.Id(), res) + } + + if !d.Get("skip_wait").(bool) { + err = FirestoreOperationWaitTime( + config, res, project, "Updating Field", userAgent, + d.Timeout(schema.TimeoutUpdate)) + + if err != nil { + return err + } + } +} + +return resourceFirestoreFieldRead(d, meta) diff --git a/mmv1/templates/terraform/samples/services/firestore/firestore_field_skip_wait.tf.tmpl b/mmv1/templates/terraform/samples/services/firestore/firestore_field_skip_wait.tf.tmpl new file mode 100644 index 000000000000..20f4bced7dd3 --- /dev/null +++ b/mmv1/templates/terraform/samples/services/firestore/firestore_field_skip_wait.tf.tmpl @@ -0,0 +1,28 @@ +resource "google_firestore_database" "database" { + project = "{{index $.TestEnvVars "project_id"}}" + name = "{{index $.ResourceIdVars "database_id"}}" + location_id = "nam5" + type = "FIRESTORE_NATIVE" + + delete_protection_state = "{{index $.ResourceIdVars "delete_protection_state"}}" + deletion_policy = "DELETE" +} + +resource "google_firestore_field" "{{$.PrimaryResourceId}}" { + project = "{{index $.TestEnvVars "project_id"}}" + database = google_firestore_database.database.name + collection = "chatrooms_%{random_suffix}" + field = "skip_wait" + + index_config { + indexes { + order = "ASCENDING" + query_scope = "COLLECTION_GROUP" + } + indexes { + array_config = "CONTAINS" + } + } + + skip_wait = true +} From 1ceefe94fe2e98e2ab61ff696cb2285296fd024e Mon Sep 17 00:00:00 2001 From: sumanth-g31 Date: Wed, 8 Jul 2026 00:13:33 +0530 Subject: [PATCH 47/78] compute: fix preview=false updates for OrganizationSecurityPolicyRule (#18079) --- mmv1/products/compute/OrganizationSecurityPolicyRule.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/mmv1/products/compute/OrganizationSecurityPolicyRule.yaml b/mmv1/products/compute/OrganizationSecurityPolicyRule.yaml index 0da489f77967..1dd50c091c6e 100644 --- a/mmv1/products/compute/OrganizationSecurityPolicyRule.yaml +++ b/mmv1/products/compute/OrganizationSecurityPolicyRule.yaml @@ -338,6 +338,7 @@ properties: type: String - name: 'preview' type: Boolean + send_empty_value: true description: | If set to true, the specified action is not enforced. - name: 'redirectOptions' From 030a2ef221ec6b533659c182da5809c04204cbb8 Mon Sep 17 00:00:00 2001 From: TOGASHI Tomoki <81744248+toga4@users.noreply.github.com> Date: Wed, 8 Jul 2026 03:53:18 +0900 Subject: [PATCH 48/78] privilegedaccessmanager: fix permadiff on entitlements with no approval workflow (#18117) --- .../privilegedaccessmanager/Entitlement.yaml | 4 + ...ager_entitlement_approval_workflow.go.tmpl | 150 ++++++++++++++++++ ...ed_access_manager_entitlement_test.go.tmpl | 52 ++++++ 3 files changed, 206 insertions(+) create mode 100644 mmv1/templates/terraform/custom_flatten/privileged_access_manager_entitlement_approval_workflow.go.tmpl diff --git a/mmv1/products/privilegedaccessmanager/Entitlement.yaml b/mmv1/products/privilegedaccessmanager/Entitlement.yaml index 754bfe83005d..8f07efa2dbc5 100644 --- a/mmv1/products/privilegedaccessmanager/Entitlement.yaml +++ b/mmv1/products/privilegedaccessmanager/Entitlement.yaml @@ -127,6 +127,10 @@ properties: The approvals needed before access will be granted to a requester. No approvals will be needed if this field is null. Different types of approval workflows that can be used to gate privileged access granting. immutable: true + # This flattener is entirely handwritten and must be updated with any new field or subfield. + # Solves a permadiff when the API returns `{"approvalWorkflow": {"manualApprovals": {}}}` + # for entitlements created without an approval workflow. + custom_flatten: templates/terraform/custom_flatten/privileged_access_manager_entitlement_approval_workflow.go.tmpl properties: - name: manualApprovals type: NestedObject diff --git a/mmv1/templates/terraform/custom_flatten/privileged_access_manager_entitlement_approval_workflow.go.tmpl b/mmv1/templates/terraform/custom_flatten/privileged_access_manager_entitlement_approval_workflow.go.tmpl new file mode 100644 index 000000000000..84455013a548 --- /dev/null +++ b/mmv1/templates/terraform/custom_flatten/privileged_access_manager_entitlement_approval_workflow.go.tmpl @@ -0,0 +1,150 @@ +{{/* + The license inside this block applies to this file + 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. +*/ -}} +func flattenPrivilegedAccessManagerEntitlementApprovalWorkflow(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + if v == nil { + return nil + } + original := v.(map[string]interface{}) + if len(original) == 0 { + return nil + } + transformed := make(map[string]interface{}) + transformed["manual_approvals"] = + flattenPrivilegedAccessManagerEntitlementApprovalWorkflowManualApprovals(original["manualApprovals"], d, config) + + // begin handwritten code (all other parts of this file are forked from generated code) + // solve for the following diff when no approval workflow is configured: + // + // - approval_workflow { + // - manual_approvals = null + // } + // + // the PAM API returns the field as: + // + // "approvalWorkflow": { + // "manualApprovals": {} + // }, + // + // for entitlements that were created without an approval workflow (for example via + // gcloud or the Cloud console). the outer `len(original) == 0` guard above does not + // fire because the `manualApprovals` key is present, and the inner flattener returns + // nil for the empty `manualApprovals` object. without this guard the resource ends + // up with a phantom `approval_workflow = [{manual_approvals = null}]` block in state, + // which cannot be reconciled with any user configuration (`manual_approvals` is + // Required, `manual_approvals.steps` has MinItems: 1, and `approval_workflow` is + // ForceNew), producing a permanent destroy+recreate diff. + // + // if any new sibling field is added under approvalWorkflow, this block must be updated + // to also check that the new field is nil before short-circuiting. + if transformed["manual_approvals"] == nil { + return nil + } + // end handwritten code + + return []interface{}{transformed} +} +func flattenPrivilegedAccessManagerEntitlementApprovalWorkflowManualApprovals(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + if v == nil { + return nil + } + original := v.(map[string]interface{}) + if len(original) == 0 { + return nil + } + transformed := make(map[string]interface{}) + transformed["require_approver_justification"] = + flattenPrivilegedAccessManagerEntitlementApprovalWorkflowManualApprovalsRequireApproverJustification(original["requireApproverJustification"], d, config) + transformed["steps"] = + flattenPrivilegedAccessManagerEntitlementApprovalWorkflowManualApprovalsSteps(original["steps"], d, config) + return []interface{}{transformed} +} +func flattenPrivilegedAccessManagerEntitlementApprovalWorkflowManualApprovalsRequireApproverJustification(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + return v +} + +func flattenPrivilegedAccessManagerEntitlementApprovalWorkflowManualApprovalsSteps(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + if v == nil { + return v + } + l := v.([]interface{}) + transformed := 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 + } + transformed = append(transformed, map[string]interface{}{ + "approvers": flattenPrivilegedAccessManagerEntitlementApprovalWorkflowManualApprovalsStepsApprovers(original["approvers"], d, config), + "approvals_needed": flattenPrivilegedAccessManagerEntitlementApprovalWorkflowManualApprovalsStepsApprovalsNeeded(original["approvalsNeeded"], d, config), + "approver_email_recipients": flattenPrivilegedAccessManagerEntitlementApprovalWorkflowManualApprovalsStepsApproverEmailRecipients(original["approverEmailRecipients"], d, config), +{{- if ne $.ResourceMetadata.TargetVersionName "ga" }} + "id": flattenPrivilegedAccessManagerEntitlementApprovalWorkflowManualApprovalsStepsId(original["id"], d, config), +{{- end }} + }) + } + return transformed +} +func flattenPrivilegedAccessManagerEntitlementApprovalWorkflowManualApprovalsStepsApprovers(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + if v == nil { + return v + } + l := v.([]interface{}) + transformed := 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 + } + transformed = append(transformed, map[string]interface{}{ + "principals": flattenPrivilegedAccessManagerEntitlementApprovalWorkflowManualApprovalsStepsApproversPrincipals(original["principals"], d, config), + }) + } + return transformed +} +func flattenPrivilegedAccessManagerEntitlementApprovalWorkflowManualApprovalsStepsApproversPrincipals(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + if v == nil { + return v + } + return schema.NewSet(schema.HashString, v.([]interface{})) +} + +func flattenPrivilegedAccessManagerEntitlementApprovalWorkflowManualApprovalsStepsApprovalsNeeded(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + // Handles the string fixed64 format + if strVal, ok := v.(string); ok { + if intVal, err := tpgresource.StringToFixed64(strVal); err == nil { + return intVal + } + } + + // number values are represented as float64 + if floatVal, ok := v.(float64); ok { + intVal := int(floatVal) + return intVal + } + + return v // let terraform core handle it otherwise +} + +func flattenPrivilegedAccessManagerEntitlementApprovalWorkflowManualApprovalsStepsApproverEmailRecipients(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + if v == nil { + return v + } + return schema.NewSet(schema.HashString, v.([]interface{})) +} +{{ if ne $.ResourceMetadata.TargetVersionName "ga" }} +func flattenPrivilegedAccessManagerEntitlementApprovalWorkflowManualApprovalsStepsId(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + return v +} +{{- end }} diff --git a/mmv1/third_party/terraform/services/privilegedaccessmanager/resource_privileged_access_manager_entitlement_test.go.tmpl b/mmv1/third_party/terraform/services/privilegedaccessmanager/resource_privileged_access_manager_entitlement_test.go.tmpl index e5ae3f989288..37a3638f4a08 100644 --- a/mmv1/third_party/terraform/services/privilegedaccessmanager/resource_privileged_access_manager_entitlement_test.go.tmpl +++ b/mmv1/third_party/terraform/services/privilegedaccessmanager/resource_privileged_access_manager_entitlement_test.go.tmpl @@ -10,6 +10,32 @@ import ( "github.com/hashicorp/terraform-provider-google/google/envvar" ) +func TestAccPrivilegedAccessManagerEntitlement_noApprovalWorkflow(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "random_suffix": acctest.RandString(t, 10), + "project_name": envvar.GetTestProjectFromEnv(), + } + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccCheckPrivilegedAccessManagerEntitlementDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccPrivilegedAccessManagerEntitlement_noApprovalWorkflow(context), + }, + { + ResourceName: "google_privileged_access_manager_entitlement.tfentitlement", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"location", "entitlement_id", "parent"}, + }, + }, + }) +} + func TestAccPrivilegedAccessManagerEntitlement_privilegedAccessManagerEntitlementProjectExample_update(t *testing.T) { t.Parallel() @@ -203,3 +229,29 @@ resource "google_privileged_access_manager_entitlement" "tfentitlement" { } `, context) } + +func testAccPrivilegedAccessManagerEntitlement_noApprovalWorkflow(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_privileged_access_manager_entitlement" "tfentitlement" { + entitlement_id = "tf-test-example-entitlement%{random_suffix}" + location = "global" + max_request_duration = "43200s" + parent = "projects/%{project_name}" + requester_justification_config { + unstructured {} + } + eligible_users { + principals = ["group:test@google.com"] + } + privileged_access { + gcp_iam_access { + role_bindings { + role = "roles/storage.admin" + } + resource = "//cloudresourcemanager.googleapis.com/projects/%{project_name}" + resource_type = "cloudresourcemanager.googleapis.com/Project" + } + } +} +`, context) +} From 6cda9eb8c8db081d099282e5aa121a12b38a73be Mon Sep 17 00:00:00 2001 From: Majed Alfaraj <98921162+majedalfaraj@users.noreply.github.com> Date: Tue, 7 Jul 2026 11:56:07 -0700 Subject: [PATCH 49/78] fix(magician): rename functions to match Google's developer's word list (#18118) --- .ci/magician/cmd/sandbox_sanity_test.go | 10 +++++----- .ci/magician/cmd/sandbox_test.go | 2 +- .ci/magician/cmd/wait_for_commit_test.go | 2 ++ 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.ci/magician/cmd/sandbox_sanity_test.go b/.ci/magician/cmd/sandbox_sanity_test.go index 84a9e3dfe2ae..b06bc73b5357 100644 --- a/.ci/magician/cmd/sandbox_sanity_test.go +++ b/.ci/magician/cmd/sandbox_sanity_test.go @@ -42,16 +42,16 @@ func TestSandboxInitialization(t *testing.T) { func TestSandboxInterceptor_AllowedCommand(t *testing.T) { sb := newSandbox(t) - sb.RequireWhitelist() + sb.RequireAllowlist() _, err := sb.Runner.Run("touch", []string{"test_file.txt"}, nil) if err != nil { - t.Fatalf("Expected whitelisted command to succeed, got: %v", err) + t.Fatalf("Expected allowlisted command to succeed, got: %v", err) } } func TestSandboxInterceptor_MockOverridesAllowed(t *testing.T) { sb := newSandbox(t) - sb.RequireWhitelist() + sb.RequireAllowlist() sb.Runner.WriteFile("echo", "#!/bin/bash\necho 'intercepted echo'") os.Chmod(filepath.Join(sb.Dir, "echo"), 0755) @@ -60,13 +60,13 @@ func TestSandboxInterceptor_MockOverridesAllowed(t *testing.T) { t.Fatalf("Expected mocked command to succeed, got: %v", err) } if !strings.Contains(output, "intercepted echo") { - t.Errorf("Expected sandbox mock to override whitelist, got: %s", output) + t.Errorf("Expected sandbox mock to override allowlist, got: %s", output) } } func TestSandboxInterceptor_UnhandledCommandPanics(t *testing.T) { sb := newSandbox(t) - sb.RequireWhitelist() + sb.RequireAllowlist() _, err := sb.Runner.Run("curl", []string{"http://example.com"}, nil) if err == nil { t.Fatalf("Expected unhandled command to return error, but it succeeded") diff --git a/.ci/magician/cmd/sandbox_test.go b/.ci/magician/cmd/sandbox_test.go index 04c0dfd0454c..1e456357e645 100644 --- a/.ci/magician/cmd/sandbox_test.go +++ b/.ci/magician/cmd/sandbox_test.go @@ -115,7 +115,7 @@ func (s *sandbox) AllowPassthrough(commands ...string) { } } -func (s *sandbox) RequireWhitelist() { +func (s *sandbox) RequireAllowlist() { if runner, ok := s.Runner.(*interceptingRunner); ok { runner.strictMode = true } diff --git a/.ci/magician/cmd/wait_for_commit_test.go b/.ci/magician/cmd/wait_for_commit_test.go index 054b5c841ad3..800f80b93717 100644 --- a/.ci/magician/cmd/wait_for_commit_test.go +++ b/.ci/magician/cmd/wait_for_commit_test.go @@ -87,6 +87,8 @@ func TestExecWaitForCommit(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() sb := newSandbox(t) + sb.RequireAllowlist() + sb.AllowPassthrough("git") originDir := t.TempDir() sb.Runner.MustRun("git", []string{"init", "--bare", "-b", "main", originDir}, nil) sb.Runner.MustRun("git", []string{"remote", "add", "origin", originDir}, nil) From ebf94d11a6efe93508aeedae4e09481d39f5b001 Mon Sep 17 00:00:00 2001 From: hannahjiang-0415 Date: Tue, 7 Jul 2026 12:37:09 -0700 Subject: [PATCH 50/78] =?UTF-8?q?Add=20psc=5Fauto=5Fconnection=5Fpolicy=5F?= =?UTF-8?q?enabled=20and=20output-only=20fields=20to=20Clou=E2=80=A6=20(#1?= =?UTF-8?q?7962)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resource_sql_database_instance.go.tmpl | 21 ++ ...ource_sql_database_instance_meta.yaml.tmpl | 3 + ...esource_sql_database_instance_test.go.tmpl | 234 +++++++++++++++++- .../r/sql_database_instance.html.markdown | 6 + 4 files changed, 254 insertions(+), 10 deletions(-) diff --git a/mmv1/third_party/terraform/services/sql/resource_sql_database_instance.go.tmpl b/mmv1/third_party/terraform/services/sql/resource_sql_database_instance.go.tmpl index 245dbcc43e99..aa611b4a5550 100644 --- a/mmv1/third_party/terraform/services/sql/resource_sql_database_instance.go.tmpl +++ b/mmv1/third_party/terraform/services/sql/resource_sql_database_instance.go.tmpl @@ -677,6 +677,12 @@ API (for read pools, effective_availability_type may differ from availability_ty Computed: true, Description: `Whether PSC write endpoint DNS is enabled for this instance.`, }, + "psc_auto_connection_policy_enabled": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + Description: `Whether a service connection policy is created for the auto connections configured for the instance.`, + }, "allowed_consumer_projects": { Type: schema.TypeSet, Optional: true, @@ -699,6 +705,7 @@ API (for read pools, effective_availability_type may differ from availability_ty "consumer_service_project_id": { Type: schema.TypeString, Optional: true, + Computed: true, Description: `The project ID of consumer service project of this consumer endpoint.`, }, "consumer_network": { @@ -721,6 +728,16 @@ API (for read pools, effective_availability_type may differ from availability_ty Computed: true, Description: `The connection status of the consumer endpoint.`, }, + "service_connection_policy": { + Type: schema.TypeString, + Computed: true, + Description: `The service connection policy created for the auto connection.`, + }, + "service_connection_policy_creation_result": { + Type: schema.TypeString, + Computed: true, + Description: `The result of the service connection policy creation.`, + }, }, }, Description: `A comma-separated list of networks or a comma-separated list of network-project pairs. Each project in this list is represented by a project number (numeric) or by a project ID (alphanumeric). This allows Private Service Connect connections to be created automatically for the specified networks.`, @@ -2137,6 +2154,7 @@ func expandPscConfig(configured []interface{}) *sqladmin.PscConfig { PscEnabled: _entry["psc_enabled"].(bool), PscAutoDnsEnabled: _entry["psc_auto_dns_enabled"].(bool), PscWriteEndpointDnsEnabled: _entry["psc_write_endpoint_dns_enabled"].(bool), + PscAutoConnectionPolicyEnabled: _entry["psc_auto_connection_policy_enabled"].(bool), AllowedConsumerProjects: tpgresource.ConvertStringArr(_entry["allowed_consumer_projects"].(*schema.Set).List()), NetworkAttachmentUri: _entry["network_attachment_uri"].(string), PscAutoConnections: expandPscAutoConnectionConfig(_entry["psc_auto_connections"].([]interface{})), @@ -3533,6 +3551,8 @@ func flattenPscAutoConnections(pscAutoConnections []*sqladmin.PscAutoConnectionC "consumer_service_project_id": flag.ConsumerProject, "ip_address": flag.IpAddress, "status": flag.Status, + "service_connection_policy": flag.ServiceConnectionPolicy, + "service_connection_policy_creation_result": flag.ServiceConnectionPolicyCreationResult, } flags = append(flags, data) @@ -3546,6 +3566,7 @@ func flattenPscConfigs(pscConfig *sqladmin.PscConfig) interface{} { "psc_enabled": pscConfig.PscEnabled, "psc_auto_dns_enabled": pscConfig.PscAutoDnsEnabled, "psc_write_endpoint_dns_enabled": pscConfig.PscWriteEndpointDnsEnabled, + "psc_auto_connection_policy_enabled": pscConfig.PscAutoConnectionPolicyEnabled, "allowed_consumer_projects": schema.NewSet(schema.HashString, tpgresource.ConvertStringArrToInterface(pscConfig.AllowedConsumerProjects)), "network_attachment_uri": pscConfig.NetworkAttachmentUri, "psc_auto_connections": flattenPscAutoConnections(pscConfig.PscAutoConnections), diff --git a/mmv1/third_party/terraform/services/sql/resource_sql_database_instance_meta.yaml.tmpl b/mmv1/third_party/terraform/services/sql/resource_sql_database_instance_meta.yaml.tmpl index 1dc442491f97..e60e3da2b6bc 100644 --- a/mmv1/third_party/terraform/services/sql/resource_sql_database_instance_meta.yaml.tmpl +++ b/mmv1/third_party/terraform/services/sql/resource_sql_database_instance_meta.yaml.tmpl @@ -175,8 +175,11 @@ fields: field: 'settings.ip_configuration.psc_config.psc_auto_connections.consumer_service_project_id' - api_field: 'settings.ipConfiguration.pscConfig.pscAutoConnections.ipAddress' - api_field: 'settings.ipConfiguration.pscConfig.pscAutoConnections.status' + - api_field: 'settings.ipConfiguration.pscConfig.pscAutoConnections.serviceConnectionPolicy' + - api_field: 'settings.ipConfiguration.pscConfig.pscAutoConnections.serviceConnectionPolicyCreationResult' - api_field: 'settings.ipConfiguration.pscConfig.pscAutoDnsEnabled' - api_field: 'settings.ipConfiguration.pscConfig.pscWriteEndpointDnsEnabled' + - api_field: 'settings.ipConfiguration.pscConfig.pscAutoConnectionPolicyEnabled' - api_field: 'settings.ipConfiguration.pscConfig.pscEnabled' - api_field: 'settings.ipConfiguration.serverCaMode' - api_field: 'settings.ipConfiguration.serverCaPool' diff --git a/mmv1/third_party/terraform/services/sql/resource_sql_database_instance_test.go.tmpl b/mmv1/third_party/terraform/services/sql/resource_sql_database_instance_test.go.tmpl index e466ee274bf1..856e18ecc5a1 100644 --- a/mmv1/third_party/terraform/services/sql/resource_sql_database_instance_test.go.tmpl +++ b/mmv1/third_party/terraform/services/sql/resource_sql_database_instance_test.go.tmpl @@ -1238,6 +1238,37 @@ func TestAccSqlDatabaseInstance_updateMCPEnabled(t *testing.T) { }) } +func TestAccSqlDatabaseInstance_withPSCEnabled_withAutoConnectionPolicy(t *testing.T) { + t.Parallel() + + instanceName := "tf-test-" + acctest.RandString(t, 10) + networkName := "tf-test-" + acctest.RandString(t, 10) + projectId := envvar.GetTestProjectFromEnv() + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccSqlDatabaseInstanceDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccSqlDatabaseInstance_withPSCEnabled_withAutoConnectionPolicy(instanceName, networkName, projectId), + Check: resource.ComposeTestCheckFunc(verifyPscOperation("google_sql_database_instance.instance", true, true, nil, false, false, true, "", "COMPLETED")), + }, + { + ResourceName: "google_sql_database_instance.instance", + ImportState: true, + ImportStateVerify: true, + ImportStateIdPrefix: fmt.Sprintf("%s/", projectId), + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + { + Config: testAccSqlDatabaseInstance_withPSCEnabled_withAutoConnectionPolicy_deleted(networkName, projectId), + Check: testAccCheckSqlDatabaseInstanceDeleteServiceConnectionPolicyByNetwork(t, projectId, "us-south1", networkName), + }, + }, + }) +} + func TestAccSqlDatabaseInstance_withPSCEnabled_withoutAllowedConsumerProjects(t *testing.T) { t.Parallel() @@ -1253,7 +1284,7 @@ func TestAccSqlDatabaseInstance_withPSCEnabled_withoutAllowedConsumerProjects(t Steps: []resource.TestStep{ { Config: testAccSqlDatabaseInstance_withPSCEnabled_withoutAllowedConsumerProjects(instanceName, projectId, orgId, billingAccount), - Check: resource.ComposeTestCheckFunc(verifyPscOperation("google_sql_database_instance.instance", true, true, nil, false, false)), + Check: resource.ComposeTestCheckFunc(verifyPscOperation("google_sql_database_instance.instance", true, true, nil, false, false, false, "", "")), }, { ResourceName: "google_sql_database_instance.instance", @@ -1281,7 +1312,7 @@ func TestAccSqlDatabaseInstance_withPSCEnabled_withEmptyAllowedConsumerProjects( Steps: []resource.TestStep{ { Config: testAccSqlDatabaseInstance_withPSCEnabled_withEmptyAllowedConsumerProjects(instanceName, projectId, orgId, billingAccount), - Check: resource.ComposeTestCheckFunc(verifyPscOperation("google_sql_database_instance.instance", true, true, []string{}, false, false)), + Check: resource.ComposeTestCheckFunc(verifyPscOperation("google_sql_database_instance.instance", true, true, []string{}, false, false, false, "", "")), }, { ResourceName: "google_sql_database_instance.instance", @@ -1309,7 +1340,7 @@ func TestAccSqlDatabaseInstance_withPSCEnabled_withAllowedConsumerProjects(t *te Steps: []resource.TestStep{ { Config: testAccSqlDatabaseInstance_withPSCEnabled_withAllowedConsumerProjects(instanceName, projectId, orgId, billingAccount), - Check: resource.ComposeTestCheckFunc(verifyPscOperation("google_sql_database_instance.instance", true, true, []string{envvar.GetTestProjectFromEnv()}, false, false)), + Check: resource.ComposeTestCheckFunc(verifyPscOperation("google_sql_database_instance.instance", true, true, []string{envvar.GetTestProjectFromEnv()}, false, false, false, "", "")), }, { ResourceName: "google_sql_database_instance.instance", @@ -1336,7 +1367,7 @@ func TestAccSqlDatabaseInstance_pscDnsConfig(t *testing.T) { { Config: testAccSqlDatabaseInstance_withPSCDnsEnabled(instanceName, projectId), Check: resource.ComposeTestCheckFunc( - verifyPscOperation("google_sql_database_instance.instance", true, true, []string{projectId}, true, true), + verifyPscOperation("google_sql_database_instance.instance", true, true, []string{projectId}, true, true, false, "", ""), ), }, { @@ -1348,7 +1379,7 @@ func TestAccSqlDatabaseInstance_pscDnsConfig(t *testing.T) { { Config: testAccSqlDatabaseInstance_withPSCDnsDisabled(instanceName, projectId), Check: resource.ComposeTestCheckFunc( - verifyPscOperation("google_sql_database_instance.instance", true, true, []string{projectId}, false, false), + verifyPscOperation("google_sql_database_instance.instance", true, true, []string{projectId}, false, false, false, "", ""), ), }, { @@ -1360,7 +1391,7 @@ func TestAccSqlDatabaseInstance_pscDnsConfig(t *testing.T) { { Config: testAccSqlDatabaseInstance_withPSCOnlyAutoDnsEnabled(instanceName, projectId), Check: resource.ComposeTestCheckFunc( - verifyPscOperation("google_sql_database_instance.instance", true, true, []string{projectId}, true, false), + verifyPscOperation("google_sql_database_instance.instance", true, true, []string{projectId}, true, false, false, "", ""), ), }, { @@ -1388,7 +1419,7 @@ func TestAccSqlDatabaseInstance_withPSCEnabled_thenAddAllowedConsumerProjects_th Steps: []resource.TestStep{ { Config: testAccSqlDatabaseInstance_withPSCEnabled_withoutAllowedConsumerProjects(instanceName, projectId, orgId, billingAccount), - Check: resource.ComposeTestCheckFunc(verifyPscOperation("google_sql_database_instance.instance", true, true, nil, false, false)), + Check: resource.ComposeTestCheckFunc(verifyPscOperation("google_sql_database_instance.instance", true, true, nil, false, false, false, "", "")), }, { ResourceName: "google_sql_database_instance.instance", @@ -1399,7 +1430,7 @@ func TestAccSqlDatabaseInstance_withPSCEnabled_thenAddAllowedConsumerProjects_th }, { Config: testAccSqlDatabaseInstance_withPSCEnabled_withAllowedConsumerProjects(instanceName, projectId, orgId, billingAccount), - Check: resource.ComposeTestCheckFunc(verifyPscOperation("google_sql_database_instance.instance", true, true, []string{envvar.GetTestProjectFromEnv()}, false, false)), + Check: resource.ComposeTestCheckFunc(verifyPscOperation("google_sql_database_instance.instance", true, true, []string{envvar.GetTestProjectFromEnv()}, false, false, false, "", "")), }, { ResourceName: "google_sql_database_instance.instance", @@ -1410,7 +1441,7 @@ func TestAccSqlDatabaseInstance_withPSCEnabled_thenAddAllowedConsumerProjects_th }, { Config: testAccSqlDatabaseInstance_withPSCEnabled_withoutAllowedConsumerProjects(instanceName, projectId, orgId, billingAccount), - Check: resource.ComposeTestCheckFunc(verifyPscOperation("google_sql_database_instance.instance", true, true, []string{}, false, false)), + Check: resource.ComposeTestCheckFunc(verifyPscOperation("google_sql_database_instance.instance", true, true, []string{}, false, false, false, "", "")), }, { ResourceName: "google_sql_database_instance.instance", @@ -7213,6 +7244,95 @@ resource "google_sql_database_instance" "instance" { `, projectId, projectId, orgId, billingAccount, instanceName) } +func testAccSqlDatabaseInstance_withPSCEnabled_withAutoConnectionPolicy(instanceName string, networkName string, projectId string) string { + return fmt.Sprintf(` +resource "google_project_service_identity" "gcp_sa_cloud_sql" { + project = "%s" + service = "sqladmin.googleapis.com" +} + +resource "google_project_iam_member" "sa_consumer_network_admin" { + project = "%s" + role = "roles/networkconnectivity.consumerNetworkAdmin" + member = google_project_service_identity.gcp_sa_cloud_sql.member +} + +resource "google_project_iam_member" "sa_network_viewer" { + project = "%s" + role = "roles/compute.networkViewer" + member = google_project_service_identity.gcp_sa_cloud_sql.member +} + +resource "google_compute_network" "default" { + project = "%s" + name = "%s" + auto_create_subnetworks = true +} + +resource "google_sql_database_instance" "instance" { + project = "%s" + name = "%s" + region = "us-south1" + database_version = "MYSQL_8_0" + deletion_protection = false + settings { + tier = "db-g1-small" + ip_configuration { + psc_config { + psc_enabled = true + psc_auto_connection_policy_enabled = true + psc_auto_connections { + consumer_network = google_compute_network.default.id + consumer_service_project_id = "%s" + } + } + ipv4_enabled = false + } + backup_configuration { + enabled = true + binary_log_enabled = true + } + database_flags { + name = "cloudsql_iam_authentication" + value = "on" + } + availability_type = "REGIONAL" + } + depends_on = [ + google_project_iam_member.sa_consumer_network_admin, + google_project_iam_member.sa_network_viewer, + ] +} +`, projectId, projectId, projectId, projectId, networkName, projectId, instanceName, projectId) +} + +func testAccSqlDatabaseInstance_withPSCEnabled_withAutoConnectionPolicy_deleted(networkName string, projectId string) string { + return fmt.Sprintf(` +resource "google_project_service_identity" "gcp_sa_cloud_sql" { + project = "%s" + service = "sqladmin.googleapis.com" +} + +resource "google_project_iam_member" "sa_consumer_network_admin" { + project = "%s" + role = "roles/networkconnectivity.consumerNetworkAdmin" + member = google_project_service_identity.gcp_sa_cloud_sql.member +} + +resource "google_project_iam_member" "sa_network_viewer" { + project = "%s" + role = "roles/compute.networkViewer" + member = google_project_service_identity.gcp_sa_cloud_sql.member +} + +resource "google_compute_network" "default" { + project = "%s" + name = "%s" + auto_create_subnetworks = true +} +`, projectId, projectId, projectId, projectId, networkName) +} + func testAccSqlDatabaseInstance_withPSCEnabled_withoutAllowedConsumerProjects(instanceName string, projectId string, orgId string, billingAccount string) string { return fmt.Sprintf(` resource "google_project" "testproject" { @@ -7407,7 +7527,7 @@ resource "google_sql_database_instance" "instance" { `, projectId, instanceName, projectId) } -func verifyPscOperation(resourceName string, isPscConfigExpected bool, expectedPscEnabled bool, expectedAllowedConsumerProjects []string, expectedPscAutoDnsEnabled bool, expectedPscWriteEndpointDnsEnabled bool) func(*terraform.State) error { +func verifyPscOperation(resourceName string, isPscConfigExpected bool, expectedPscEnabled bool, expectedAllowedConsumerProjects []string, expectedPscAutoDnsEnabled bool, expectedPscWriteEndpointDnsEnabled bool, expectedEnablePscAutoConnectionPolicy bool, expectedServiceConnectionPolicy string, expectedServiceConnectionPolicyCreationResult string) func(*terraform.State) error { return func(s *terraform.State) error { resource, ok := s.RootModule().Resources[resourceName] if !ok { @@ -7444,6 +7564,26 @@ func verifyPscOperation(resourceName string, isPscConfigExpected bool, expectedP return fmt.Errorf("settings.0.ip_configuration.0.psc_config.0.psc_write_endpoint_dns_enabled property value is not set as expected in state of %s, expected %v, actual %v", resourceName, expectedPscWriteEndpointDnsEnabled, pscWriteEndpointDnsEnabled) } + enablePscAutoConnectionPolicyStr, ok := resourceAttributes["settings.0.ip_configuration.0.psc_config.0.psc_auto_connection_policy_enabled"] + enablePscAutoConnectionPolicy, err := strconv.ParseBool(enablePscAutoConnectionPolicyStr) + if err != nil || enablePscAutoConnectionPolicy != expectedEnablePscAutoConnectionPolicy { + return fmt.Errorf("settings.0.ip_configuration.0.psc_config.0.psc_auto_connection_policy_enabled property value is not set as expected in state of %s, expected %v, actual %v", resourceName, expectedEnablePscAutoConnectionPolicy, enablePscAutoConnectionPolicy) + } + + if expectedServiceConnectionPolicy != "" { + serviceConnectionPolicy, ok := resourceAttributes["settings.0.ip_configuration.0.psc_config.0.psc_auto_connections.0.service_connection_policy"] + if !ok || serviceConnectionPolicy != expectedServiceConnectionPolicy { + return fmt.Errorf("settings.0.ip_configuration.0.psc_config.0.psc_auto_connections.0.service_connection_policy property value is not set as expected in state of %s, expected %v, actual %v", resourceName, expectedServiceConnectionPolicy, serviceConnectionPolicy) + } + } + + if expectedServiceConnectionPolicyCreationResult != "" { + serviceConnectionPolicyCreationResult, ok := resourceAttributes["settings.0.ip_configuration.0.psc_config.0.psc_auto_connections.0.service_connection_policy_creation_result"] + if !ok || serviceConnectionPolicyCreationResult != expectedServiceConnectionPolicyCreationResult { + return fmt.Errorf("settings.0.ip_configuration.0.psc_config.0.psc_auto_connections.0.service_connection_policy_creation_result property value is not set as expected in state of %s, expected %v, actual %v", resourceName, expectedServiceConnectionPolicyCreationResult, serviceConnectionPolicyCreationResult) + } + } + allowedConsumerProjectsStr, ok := resourceAttributes["settings.0.ip_configuration.0.psc_config.0.allowed_consumer_projects.#"] allowedConsumerProjects, err := strconv.Atoi(allowedConsumerProjectsStr) if !ok || allowedConsumerProjects != len(expectedAllowedConsumerProjects) { @@ -7529,6 +7669,80 @@ func verifyPscAutoConnectionsOperation(resourceName string, isPscConfigExpected } } +func testAccCheckSqlDatabaseInstanceDeleteServiceConnectionPolicyByNetwork(t *testing.T, project string, region string, networkName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + config := acctest.GoogleProviderConfig(t) + url := fmt.Sprintf("https://networkconnectivity.googleapis.com/v1/projects/%s/locations/%s/serviceConnectionPolicies", project, region) + + billingProject := "" + if config.BillingProject != "" { + billingProject = config.BillingProject + } + + res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "GET", + Project: billingProject, + RawURL: url, + UserAgent: config.UserAgent, + }) + if err != nil { + log.Printf("[DEBUG] Error listing existing service connection policies: %s", err) + return nil + } + if res == nil || res["serviceConnectionPolicies"] == nil { + return nil + } + + policies, ok := res["serviceConnectionPolicies"].([]interface{}) + if !ok { + return nil + } + for _, p := range policies { + policy, ok := p.(map[string]interface{}) + if !ok { + continue + } + network, ok := policy["network"].(string) + if !ok || (!strings.HasSuffix(network, "/networks/"+networkName) && !strings.Contains(network, "/"+networkName+"")) { + continue + } + name, ok := policy["name"].(string) + if !ok || name == "" { + continue + } + deleteUrl := fmt.Sprintf("https://networkconnectivity.googleapis.com/v1/%s", strings.TrimPrefix(name, "/")) + log.Printf("[DEBUG] Deleting service connection policy %q before deleting network", name) + _, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "DELETE", + Project: billingProject, + RawURL: deleteUrl, + UserAgent: config.UserAgent, + }) + if err != nil && !transport_tpg.IsGoogleApiErrorWithCode(err, 404) { + log.Printf("[DEBUG] Error deleting service connection policy %s: %s", name, err) + } + + for i := 0; i < 30; i++ { + time.Sleep(5 * time.Second) + _, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "GET", + Project: billingProject, + RawURL: deleteUrl, + UserAgent: config.UserAgent, + }) + if err != nil && transport_tpg.IsGoogleApiErrorWithCode(err, 404) { + log.Printf("[DEBUG] Service connection policy %q deleted successfully", name) + break + } + } + } + return nil + } +} + func verifyPscNetorkAttachmentOperation(resourceName string, isPscConfigExpected bool, expectedPscEnabled bool, expectedNetworkAttachmentUri string) func(*terraform.State) error { return func(s *terraform.State) error { resource, ok := s.RootModule().Resources[resourceName] diff --git a/mmv1/third_party/terraform/website/docs/r/sql_database_instance.html.markdown b/mmv1/third_party/terraform/website/docs/r/sql_database_instance.html.markdown index fb02b87d898e..0743990ec828 100644 --- a/mmv1/third_party/terraform/website/docs/r/sql_database_instance.html.markdown +++ b/mmv1/third_party/terraform/website/docs/r/sql_database_instance.html.markdown @@ -620,6 +620,8 @@ The optional `settings.ip_configuration.psc_config` sublist supports: * `psc_write_endpoint_dns_enabled` - (Optional) Whether PSC write endpoint DNS is enabled for this instance. This is only supported for Enterprise Plus edition instances. +* `settings.ip_configuration.psc_config.psc_auto_connection_policy_enabled` - (Optional) Whether a service connection policy is created for the auto connections configured for the instance. + * `allowed_consumer_projects` - (Optional) List of consumer projects that are allow-listed for PSC connections to this instance. This instance can be connected to with PSC from any network in these projects. Each consumer project in this list may be represented by a project number (numeric) or by a project id (alphanumeric). * The optional `psc_config.psc_auto_connections` subblock - (Optional) A comma-separated list of networks or a comma-separated list of network-project pairs. Each project in this list is represented by a project number (numeric) or by a project ID (alphanumeric). This allows Private Service Connect connections to be created automatically for the specified networks. @@ -898,6 +900,10 @@ performing filtering in a Terraform config. * `settings.ip_configuration.psc_config.psc_auto_connections.status` - (Output) The connection status of the consumer endpoint. +* `settings.ip_configuration.psc_config.psc_auto_connections.service_connection_policy` - (Output) The service connection policy created for the auto connection. + +* `settings.ip_configuration.psc_config.psc_auto_connections.service_connection_policy_creation_result` - (Output) The result of the service connection policy creation. + * `settings.version` - Used to make sure changes to the `settings` block are atomic. From ceac39fc780a72c313295777093c1f4e4c06a9b0 Mon Sep 17 00:00:00 2001 From: Marc Gomez Date: Tue, 7 Jul 2026 14:28:20 -0600 Subject: [PATCH 51/78] Fix terraform type mismatch on nested Eventarc Trigger conditions (#18165) --- mmv1/products/eventarc/Trigger.yaml | 1 + ...ventarc_trigger_conditions_flatten.go.tmpl | 46 +++++++++++++++++++ ...latten_eventarc_trigger_conditions_test.go | 26 +++++++++++ 3 files changed, 73 insertions(+) create mode 100644 mmv1/templates/terraform/custom_flatten/eventarc_trigger_conditions_flatten.go.tmpl create mode 100644 mmv1/third_party/terraform/services/eventarc/flatten_eventarc_trigger_conditions_test.go diff --git a/mmv1/products/eventarc/Trigger.yaml b/mmv1/products/eventarc/Trigger.yaml index 5b8761ecbd8f..309984330a50 100644 --- a/mmv1/products/eventarc/Trigger.yaml +++ b/mmv1/products/eventarc/Trigger.yaml @@ -275,6 +275,7 @@ properties: type: KeyValuePairs description: Output only. The reason(s) why a trigger is in FAILED state. output: true + custom_flatten: templates/terraform/custom_flatten/eventarc_trigger_conditions_flatten.go.tmpl - name: eventDataContentType type: String description: Optional. EventDataContentType specifies the type of payload in MIME format that is expected from the CloudEvent data field. This is set to `application/json` if the value is not defined. diff --git a/mmv1/templates/terraform/custom_flatten/eventarc_trigger_conditions_flatten.go.tmpl b/mmv1/templates/terraform/custom_flatten/eventarc_trigger_conditions_flatten.go.tmpl new file mode 100644 index 000000000000..764116702353 --- /dev/null +++ b/mmv1/templates/terraform/custom_flatten/eventarc_trigger_conditions_flatten.go.tmpl @@ -0,0 +1,46 @@ +{{/* + The license inside this block applies to this file + 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. +*/ -}} +func flatten{{$.GetPrefix}}{{$.TitlelizeProperty}}(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + if v == nil { + return nil + } + + original, ok := v.(map[string]interface{}) + if !ok { + return v + } + + flatConditions := make(map[string]string) + for k, rawVal := range original { + if rawVal == nil { + continue + } + + if nestedMap, ok := rawVal.(map[string]interface{}); ok { + if message, exists := nestedMap["message"]; exists { + if msgStr, isStr := message.(string); isStr { + flatConditions[k] = msgStr + continue + } + } + } + + if valStr, ok := rawVal.(string); ok { + flatConditions[k] = valStr + } else { + flatConditions[k] = fmt.Sprintf("%v", rawVal) + } + } + + return flatConditions +} diff --git a/mmv1/third_party/terraform/services/eventarc/flatten_eventarc_trigger_conditions_test.go b/mmv1/third_party/terraform/services/eventarc/flatten_eventarc_trigger_conditions_test.go new file mode 100644 index 000000000000..b81b7fee0b28 --- /dev/null +++ b/mmv1/third_party/terraform/services/eventarc/flatten_eventarc_trigger_conditions_test.go @@ -0,0 +1,26 @@ +package eventarc + +import ( + "testing" +) + +func TestFlattenEventarcTriggerConditions(t *testing.T) { + mockApiResponse := map[string]interface{}{ + "transport.pubsub.topic": map[string]interface{}{ + "code": "UNKNOWN", + "message": "Pub/Sub topic status is unknown. Try requesting the trigger description again.", + }, + } + + result := flattenEventarcTriggerConditions(mockApiResponse, nil, nil) + + flatResult, ok := result.(map[string]string) + if !ok { + t.Fatalf("Flattener returned %T, expected map[string]string", result) + } + + expectedMessage := "Pub/Sub topic status is unknown. Try requesting the trigger description again." + if flatResult["transport.pubsub.topic"] != expectedMessage { + t.Fatalf("Expected message %q, got %q", expectedMessage, flatResult["transport.pubsub.topic"]) + } +} From 1dc1c84c418374fccd56572081af2cd37ad81af8 Mon Sep 17 00:00:00 2001 From: Ewa Miklewska <92916895+Ewa-M@users.noreply.github.com> Date: Tue, 7 Jul 2026 23:07:57 +0200 Subject: [PATCH 52/78] Add recurring maintenance window (#17899) Co-authored-by: Stephen Lewis (Burrows) --- .../resource_container_cluster.go.tmpl | 187 ++++++++++-- .../resource_container_cluster_meta.yaml.tmpl | 18 +- .../resource_container_cluster_test.go.tmpl | 276 ++++++++++++------ .../docs/r/container_cluster.html.markdown | 57 +++- .../container/resource_container_cluster.go | 80 +++++ .../resource_container_cluster_cai2hcl.go | 35 ++- .../resource_container_cluster_tfplan2cai.go | 30 ++ 7 files changed, 568 insertions(+), 115 deletions(-) diff --git a/mmv1/third_party/terraform/services/container/resource_container_cluster.go.tmpl b/mmv1/third_party/terraform/services/container/resource_container_cluster.go.tmpl index ad6522134d56..913265796e8d 100644 --- a/mmv1/third_party/terraform/services/container/resource_container_cluster.go.tmpl +++ b/mmv1/third_party/terraform/services/container/resource_container_cluster.go.tmpl @@ -38,6 +38,15 @@ func Rfc3339TimeDiffSuppress(k, old, new string, d *schema.ResourceData) bool { return false } +func validateDuration(v interface{}, k string) (ws []string, errors []error) { + s := v.(string) + _, err := time.ParseDuration(s) + if err != nil { + errors = append(errors, fmt.Errorf("%q is not a valid duration: %s", k, err)) + } + return +} + var ( instanceGroupManagerURL = regexp.MustCompile(fmt.Sprintf("projects/(%s)/zones/([a-z0-9-]*)/instanceGroupManagers/([^/]*)", verify.ProjectRegex)) @@ -140,15 +149,15 @@ var ( return true } // Suppress the addition of a fleet block (count changes 0 -> 1) if the "project" field being added is null or empty. - if k == "fleet.#" && oldValue == "0" && newValue == "1" { - // When transitioning from 0->1 blocks, d.Get/d.GetOk effectively reads the 'new' config value. - projectVal, projectIsSet := d.GetOk("fleet.0.project") - if !projectIsSet || projectVal.(string) == "" { - log.Printf("[DEBUG] Suppressing diff for 'fleet.#' (0 -> 1) because fleet.0.project is null or empty in config.\n") - return true - } - } - return false + if k == "fleet.#" && oldValue == "0" && newValue == "1" { + // When transitioning from 0->1 blocks, d.Get/d.GetOk effectively reads the 'new' config value. + projectVal, projectIsSet := d.GetOk("fleet.0.project") + if !projectIsSet || projectVal.(string) == "" { + log.Printf("[DEBUG] Suppressing diff for 'fleet.#' (0 -> 1) because fleet.0.project is null or empty in config.\n") + return true + } + } + return false }) suppressDiffForConfidentialNodes = schema.SchemaDiffSuppressFunc(func(k, oldValue, newValue string, d *schema.ResourceData) bool { @@ -1234,6 +1243,7 @@ func ResourceContainerCluster() *schema.Resource { ExactlyOneOf: []string{ "maintenance_policy.0.daily_maintenance_window", "maintenance_policy.0.recurring_window", + "maintenance_policy.0.recurring_maintenance_window", }, MaxItems: 1, Description: `Time window specified for daily maintenance operations. Specify start_time in RFC3339 format "HH:MM”, where HH : [00-23] and MM : [00-59] GMT.`, @@ -1259,6 +1269,7 @@ func ResourceContainerCluster() *schema.Resource { ExactlyOneOf: []string{ "maintenance_policy.0.daily_maintenance_window", "maintenance_policy.0.recurring_window", + "maintenance_policy.0.recurring_maintenance_window", }, Description: `Time window for recurring maintenance operations.`, Elem: &schema.Resource{ @@ -1281,6 +1292,74 @@ func ResourceContainerCluster() *schema.Resource { }, }, }, + "recurring_maintenance_window": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + ExactlyOneOf: []string{ + "maintenance_policy.0.daily_maintenance_window", + "maintenance_policy.0.recurring_window", + "maintenance_policy.0.recurring_maintenance_window", + }, + Description: `Time window for recurring maintenance operations.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "delay_until": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "year": { + Type: schema.TypeInt, + Required: true, + }, + "month": { + Type: schema.TypeInt, + Required: true, + }, + "day": { + Type: schema.TypeInt, + Required: true, + }, + }, + }, + }, + "window_duration": { + Required: true, + Type: schema.TypeString, + DiffSuppressFunc: tpgresource.DurationDiffSuppress, + ValidateFunc: validateDuration, + }, + "window_start_time": { + Required: true, + Type: schema.TypeList, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "hours": { + Type: schema.TypeInt, + Required: true, + }, + "minutes": { + Type: schema.TypeInt, + Required: true, + }, + "seconds": { + Type: schema.TypeInt, + Required: true, + }, + }, + }, + }, + "recurrence": { + Type: schema.TypeString, + Required: true, + DiffSuppressFunc: rfc5545RecurrenceDiffSuppress, + }, + }, + }, + }, "maintenance_exclusion": { Type: schema.TypeSet, Optional: true, @@ -3941,20 +4020,20 @@ func resourceContainerClusterRead(d *schema.ResourceData, meta interface{}) erro return err } - + if err := tpgresource.DeletionPolicyReadDefault(d, config, "DELETE"); err != nil{ return err } - + return nil } func resourceContainerClusterUpdate(d *schema.ResourceData, meta interface{}) error { - + if tpgresource.DeletionPolicyPreUpdate(d, ResourceContainerCluster) { return ResourceContainerCluster().Read(d, meta) } - + config := meta.(*transport_tpg.Config) userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) if err != nil { @@ -5761,13 +5840,13 @@ func resourceContainerClusterUpdate(d *schema.ResourceData, meta interface{}) er } func resourceContainerClusterDelete(d *schema.ResourceData, meta interface{}) error { - + if ok, err := tpgresource.DeletionPolicyPreDelete(d); err != nil{ return err }else if ok{ return nil } - + if d.Get("deletion_protection").(bool) { return fmt.Errorf("Cannot destroy cluster because deletion_protection is set to true. Set it to false to proceed with cluster deletion.") } @@ -6183,7 +6262,7 @@ func expandAutoIpamConfig(configured interface{}) *container.AutoIpamConfig { if !ok || len(l) == 0 || l[0] == nil { return nil } - + return &container.AutoIpamConfig{ Enabled: l[0].(map[string]interface{})["enabled"].(bool), } @@ -6304,6 +6383,35 @@ func expandMaintenancePolicy(d *schema.ResourceData, meta interface{}) *containe ResourceVersion: resourceVersion, } } + if recurringMaintenanceWindow, ok := maintenancePolicy["recurring_maintenance_window"]; ok && len(recurringMaintenanceWindow.([]interface{})) > 0 { + rmw := recurringMaintenanceWindow.([]interface{})[0].(map[string]interface{}) + duration, _ := time.ParseDuration(rmw["window_duration"].(string)) + + policy := &container.MaintenancePolicy{ + Window: &container.MaintenanceWindow{ + MaintenanceExclusions: exclusions, + RecurringMaintenanceWindow: &container.RecurringMaintenanceWindow{ + WindowStartTime: &container.TimeOfDay{ + Hours: int64(rmw["window_start_time"].([]interface{})[0].(map[string]interface{})["hours"].(int)), + Minutes: int64(rmw["window_start_time"].([]interface{})[0].(map[string]interface{})["minutes"].(int)), + Seconds: int64(rmw["window_start_time"].([]interface{})[0].(map[string]interface{})["seconds"].(int)), + }, + WindowDuration: fmt.Sprintf("%ds", int(duration.Seconds())), + Recurrence: rmw["recurrence"].(string), + }, + }, + ResourceVersion: resourceVersion, + } + + if _, ok := rmw["delay_until"]; ok && len(rmw["delay_until"].([]interface{})) == 1 { + policy.Window.RecurringMaintenanceWindow.DelayUntil = &container.Date{ + Year: int64(rmw["delay_until"].([]interface{})[0].(map[string]interface{})["year"].(int)), + Month: int64(rmw["delay_until"].([]interface{})[0].(map[string]interface{})["month"].(int)), + Day: int64(rmw["delay_until"].([]interface{})[0].(map[string]interface{})["day"].(int)), + } + } + return policy + } return nil } @@ -8158,7 +8266,7 @@ func flattenMaintenancePolicy(mp *container.MaintenancePolicy) []map[string]inte if window.EndTime != "" { exclusion["end_time"] = window.EndTime } - } + } } else { if window.EndTime != "" { exclusion["end_time"] = window.EndTime @@ -8216,6 +8324,37 @@ func flattenMaintenancePolicy(mp *container.MaintenancePolicy) []map[string]inte }, } } + if mp.Window.RecurringMaintenanceWindow != nil { + window := []map[string]interface{}{ + { + "recurring_maintenance_window": []map[string]interface{}{ + { + "window_start_time": []map[string]interface{}{ + { + "hours": mp.Window.RecurringMaintenanceWindow.WindowStartTime.Hours, + "minutes": mp.Window.RecurringMaintenanceWindow.WindowStartTime.Minutes, + "seconds": mp.Window.RecurringMaintenanceWindow.WindowStartTime.Seconds, + }, + }, + "window_duration": mp.Window.RecurringMaintenanceWindow.WindowDuration, + "recurrence": mp.Window.RecurringMaintenanceWindow.Recurrence, + }, + }, + "maintenance_exclusion": exclusions, + }, + } + + if mp.Window.RecurringMaintenanceWindow.DelayUntil != nil { + window[0]["recurring_maintenance_window"].([]map[string]interface{})[0]["delay_until"] = []map[string]interface{}{ + { + "year": mp.Window.RecurringMaintenanceWindow.DelayUntil.Year, + "month": mp.Window.RecurringMaintenanceWindow.DelayUntil.Month, + "day": mp.Window.RecurringMaintenanceWindow.DelayUntil.Day, + }, + } + } + return window + } return nil } @@ -9231,7 +9370,7 @@ func clusterAcceleratorNetworkProfileCustomizeDiff(_ context.Context, diff *sche // A. DETECT USER CONFIG (Raw Config Check for this specific pool) userHasAdditionalConfigs := false - + // Re-find the specific raw block for logic checking if !rawNodePools.IsNull() { it := rawNodePools.ElementIterator() @@ -9260,9 +9399,9 @@ func clusterAcceleratorNetworkProfileCustomizeDiff(_ context.Context, diff *sche shouldClear := false basePath := fmt.Sprintf("node_pool.%d", i) networkConfigPath := basePath + ".network_config.0" - + oldProfile, newProfile := diff.GetChange(networkConfigPath + ".accelerator_network_profile") - + newProfileStr := "" if newProfile != nil { newProfileStr = newProfile.(string) } oldProfileStr := "" @@ -9292,9 +9431,9 @@ func clusterAcceleratorNetworkProfileCustomizeDiff(_ context.Context, diff *sche // C. APPLY FIX TO THE MAP if shouldClear { log.Printf("[DEBUG] Cluster ANP CustomizeDiff: Clearing additional configs for INLINE pool %s", currentName) - + var newConfigMap map[string]interface{} - + if ncList, ok := newNpMap["network_config"].([]interface{}); ok && len(ncList) > 0 { if existingMap, ok := ncList[0].(map[string]interface{}); ok { newConfigMap = make(map[string]interface{}) @@ -9303,13 +9442,13 @@ func clusterAcceleratorNetworkProfileCustomizeDiff(_ context.Context, diff *sche } } } - + if newConfigMap == nil { newConfigMap = make(map[string]interface{}) } newConfigMap["additional_node_network_configs"] = []interface{}{} - + if !anpIsActive { newConfigMap["accelerator_network_profile"] = "" } diff --git a/mmv1/third_party/terraform/services/container/resource_container_cluster_meta.yaml.tmpl b/mmv1/third_party/terraform/services/container/resource_container_cluster_meta.yaml.tmpl index 34c5513d8f8b..c1b9589cffd4 100644 --- a/mmv1/third_party/terraform/services/container/resource_container_cluster_meta.yaml.tmpl +++ b/mmv1/third_party/terraform/services/container/resource_container_cluster_meta.yaml.tmpl @@ -230,7 +230,23 @@ fields: api_field: 'maintenancePolicy.window.recurringWindow.recurrence' - field: 'maintenance_policy.recurring_window.start_time' api_field: 'maintenancePolicy.window.recurringWindow.window.startTime' - - api_field: 'maintenancePolicy.disruptionBudget.lastDisruptionTime' + - field: 'maintenance_policy.recurring_maintenance_window.window_start_time.hours' + api_field: 'maintenancePolicy.window.recurringMaintenanceWindow.windowStartTime.hours' + - field: 'maintenance_policy.recurring_maintenance_window.window_start_time.minutes' + api_field: 'maintenancePolicy.window.recurringMaintenanceWindow.windowStartTime.minutes' + - field: 'maintenance_policy.recurring_maintenance_window.window_start_time.seconds' + api_field: 'maintenancePolicy.window.recurringMaintenanceWindow.windowStartTime.seconds' + - field: 'maintenance_policy.recurring_maintenance_window.window_duration' + api_field: 'maintenancePolicy.window.recurringMaintenanceWindow.windowDuration.seconds' + - field: 'maintenance_policy.recurring_maintenance_window.delay_until.year' + api_field: 'maintenancePolicy.window.recurringMaintenanceWindow.delayUntil.year' + - field: 'maintenance_policy.recurring_maintenance_window.delay_until.month' + api_field: 'maintenancePolicy.window.recurringMaintenanceWindow.delayUntil.month' + - field: 'maintenance_policy.recurring_maintenance_window.delay_until.day' + api_field: 'maintenancePolicy.window.recurringMaintenanceWindow.delayUntil.day' + - field: 'maintenance_policy.recurring_maintenance_window.recurrence' + api_field: 'maintenancePolicy.window.recurringMaintenanceWindow.recurrence' + - api_field: 'maintenancePolicy.disruptionBudget.lastDisruptionTime' - api_field: 'maintenancePolicy.disruptionBudget.lastMinorVersionDisruptionTime' - api_field: 'maintenancePolicy.disruptionBudget.MinorVersionDisruptionInterval' - api_field: 'maintenancePolicy.disruptionBudget.PatchVersionDisruptionInterval' diff --git a/mmv1/third_party/terraform/services/container/resource_container_cluster_test.go.tmpl b/mmv1/third_party/terraform/services/container/resource_container_cluster_test.go.tmpl index b9300ee5a431..6433b5cfa880 100644 --- a/mmv1/third_party/terraform/services/container/resource_container_cluster_test.go.tmpl +++ b/mmv1/third_party/terraform/services/container/resource_container_cluster_test.go.tmpl @@ -2538,7 +2538,7 @@ func TestAccContainerCluster_withKubeletConfig(t *testing.T) { "node_config.0.kubelet_config.0.topology_manager.0.policy", "best-effort"), resource.TestCheckResourceAttr( "google_container_cluster.with_kubelet_config", - "node_config.0.kubelet_config.0.topology_manager.0.scope", "pod"), + "node_config.0.kubelet_config.0.topology_manager.0.scope", "pod"), ), }, { @@ -3883,12 +3883,12 @@ func TestAccContainerCluster_withNodePoolMaintenancePolicy(t *testing.T) { func TestAccContainerCluster_withClusterDisruptionBudget(t *testing.T) { t.Parallel() - + clusterName := fmt.Sprintf("tf-test-cluster-%s", acctest.RandString(t, 10)) resourceName := "google_container_cluster.with_cluster_disruption_budget" networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") subnetworkName := tpgcompute.BootstrapSubnet(t, "gke-cluster", networkName) - + acctest.VcrTest(t, resource.TestCase{ PreCheck: func() { acctest.AccTestPreCheck(t) }, ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), @@ -3907,7 +3907,7 @@ func TestAccContainerCluster_withClusterDisruptionBudget(t *testing.T) { }) } -func TestAccContainerCluster_withMaintenanceWindow(t *testing.T) { +func TestAccContainerCluster_withDailyMaintenanceWindow(t *testing.T) { t.Parallel() clusterName := fmt.Sprintf("tf-test-cluster-%s", acctest.RandString(t, 10)) @@ -3921,7 +3921,7 @@ func TestAccContainerCluster_withMaintenanceWindow(t *testing.T) { CheckDestroy: testAccCheckContainerClusterDestroyProducer(t), Steps: []resource.TestStep{ { - Config: testAccContainerCluster_withMaintenanceWindow(clusterName, "03:00", networkName, subnetworkName), + Config: testAccContainerCluster_withDailyMaintenanceWindow(clusterName, "03:00", networkName, subnetworkName), }, { ResourceName: resourceName, @@ -3930,10 +3930,12 @@ func TestAccContainerCluster_withMaintenanceWindow(t *testing.T) { ImportStateVerifyIgnore: []string{"deletion_protection"}, }, { - Config: testAccContainerCluster_withMaintenanceWindow(clusterName, "", networkName, subnetworkName), + Config: testAccContainerCluster_withDailyMaintenanceWindow(clusterName, "", networkName, subnetworkName), Check: resource.ComposeTestCheckFunc( resource.TestCheckNoResourceAttr(resourceName, "maintenance_policy.0.daily_maintenance_window.0.start_time"), + resource.TestCheckNoResourceAttr(resourceName, + "maintenance_policy.0.recurring_maintenance_window.0.window_start_time"), ), }, { @@ -3948,7 +3950,7 @@ func TestAccContainerCluster_withMaintenanceWindow(t *testing.T) { }) } -func TestAccContainerCluster_withRecurringMaintenanceWindow(t *testing.T) { +func TestAccContainerCluster_withRecurringTimeWindow(t *testing.T) { t.Parallel() cluster := fmt.Sprintf("tf-test-cluster-%s", acctest.RandString(t, 10)) resourceName := "google_container_cluster.with_recurring_maintenance_window" @@ -3961,10 +3963,12 @@ func TestAccContainerCluster_withRecurringMaintenanceWindow(t *testing.T) { CheckDestroy: testAccCheckContainerClusterDestroyProducer(t), Steps: []resource.TestStep{ { - Config: testAccContainerCluster_withRecurringMaintenanceWindow(cluster, "2019-01-01T00:00:00Z", "2019-01-02T00:00:00Z", networkName, subnetworkName), + Config: testAccContainerCluster_withRecurringTimeWindow(cluster, "2019-01-01T00:00:00Z", "2019-01-02T00:00:00Z", networkName, subnetworkName), Check: resource.ComposeTestCheckFunc( resource.TestCheckNoResourceAttr(resourceName, "maintenance_policy.0.daily_maintenance_window.0.start_time"), + resource.TestCheckNoResourceAttr(resourceName, + "maintenance_policy.0.recurring_maintenance_window.0.window_start_time"), ), }, { @@ -3976,12 +3980,14 @@ func TestAccContainerCluster_withRecurringMaintenanceWindow(t *testing.T) { }, { - Config: testAccContainerCluster_withRecurringMaintenanceWindow(cluster, "", "", networkName, subnetworkName), + Config: testAccContainerCluster_withRecurringTimeWindow(cluster, "", "", networkName, subnetworkName), Check: resource.ComposeTestCheckFunc( resource.TestCheckNoResourceAttr(resourceName, "maintenance_policy.0.daily_maintenance_window.0.start_time"), resource.TestCheckNoResourceAttr(resourceName, "maintenance_policy.0.recurring_window.0.start_time"), + resource.TestCheckNoResourceAttr(resourceName, + "maintenance_policy.0.recurring_maintenance_window.0.window_start_time"), ), }, { @@ -3997,6 +4003,56 @@ func TestAccContainerCluster_withRecurringMaintenanceWindow(t *testing.T) { }) } +func TestAccContainerCluster_withRecurringMaintenanceWindow(t *testing.T) { + t.Parallel() + cluster := fmt.Sprintf("tf-test-cluster-%s", acctest.RandString(t, 10)) + resourceName := "google_container_cluster.with_recurring_maintenance_window" + networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") + subnetworkName := tpgcompute.BootstrapSubnet(t, "gke-cluster", networkName) + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccCheckContainerClusterDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccContainerCluster_withRecurringMaintenanceWindow(cluster, 2019, 1, 1, 10, 0, "24h", networkName, subnetworkName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckNoResourceAttr(resourceName, + "maintenance_policy.0.daily_maintenance_window.0.start_time"), + resource.TestCheckNoResourceAttr(resourceName, + "maintenance_policy.0.recurring_window.0.start_time"), + ), + }, + { + ResourceName: resourceName, + ImportStateIdPrefix: "us-central1-a/", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + { + Config: testAccContainerCluster_withRecurringMaintenanceWindow(cluster, 0, 0, 0, 10, 0, "4h", networkName, subnetworkName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckNoResourceAttr(resourceName, + "maintenance_policy.0.daily_maintenance_window.0.start_time"), + resource.TestCheckNoResourceAttr(resourceName, + "maintenance_policy.0.recurring_window.0.start_time"), + resource.TestCheckNoResourceAttr(resourceName, + "maintenance_policy.0.recurring_maintenance_window.0.delay_until.0.year"), + ), + }, + { + ResourceName: resourceName, + ImportStateIdPrefix: "us-central1-a/", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + }, + }) +} + func TestAccContainerCluster_withMaintenanceExclusionWindow(t *testing.T) { t.Parallel() cluster := fmt.Sprintf("tf-test-cluster-%s", acctest.RandString(t, 10)) @@ -5420,71 +5476,71 @@ func TestAccContainerCluster_withSecretManagerConfig(t *testing.T) { func TestAccContainerCluster_withSecretSyncConfig(t *testing.T) { t.Parallel() - clusterName := fmt.Sprintf("tf-test-cluster-%s", acctest.RandString(t, 10)) - networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") - subnetworkName := tpgcompute.BootstrapSubnet(t, "gke-cluster", networkName) - pid := envvar.GetTestProjectFromEnv() - acctest.VcrTest(t, resource.TestCase{ - PreCheck: func() { acctest.AccTestPreCheck(t) }, - ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), - CheckDestroy: testAccCheckContainerClusterDestroyProducer(t), - Steps: []resource.TestStep{ - { - Config: testAccContainerCluster_forSecretSyncConfig(pid, clusterName, networkName, subnetworkName), - }, - { - ResourceName: "google_container_cluster.primary", - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"deletion_protection"}, - }, - { - Config: testAccContainerCluster_withSecretSyncConfigEnabled(pid, clusterName, networkName, subnetworkName), - }, - { - ResourceName: "google_container_cluster.primary", - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"deletion_protection"}, - }, - { - Config: testAccContainerCluster_withSecretSyncRotationPeriodUpdated(pid, clusterName, networkName, subnetworkName), - }, - { - ResourceName: "google_container_cluster.primary", - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"deletion_protection"}, - }, - { - Config: testAccContainerCluster_withSecretSyncConfigRotationDisabled(pid, clusterName, networkName, subnetworkName), - }, - { - ResourceName: "google_container_cluster.primary", - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"deletion_protection"}, - }, - { - Config: testAccContainerCluster_withSecretSyncConfigDisabled(pid, clusterName, networkName, subnetworkName), - }, - { - ResourceName: "google_container_cluster.primary", - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"deletion_protection"}, - }, - { - Config: testAccContainerCluster_forSecretSyncConfig(pid, clusterName, networkName, subnetworkName), - }, - { - ResourceName: "google_container_cluster.primary", - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"deletion_protection"}, - }, - }, - }) + clusterName := fmt.Sprintf("tf-test-cluster-%s", acctest.RandString(t, 10)) + networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") + subnetworkName := tpgcompute.BootstrapSubnet(t, "gke-cluster", networkName) + pid := envvar.GetTestProjectFromEnv() + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccCheckContainerClusterDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccContainerCluster_forSecretSyncConfig(pid, clusterName, networkName, subnetworkName), + }, + { + ResourceName: "google_container_cluster.primary", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + { + Config: testAccContainerCluster_withSecretSyncConfigEnabled(pid, clusterName, networkName, subnetworkName), + }, + { + ResourceName: "google_container_cluster.primary", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + { + Config: testAccContainerCluster_withSecretSyncRotationPeriodUpdated(pid, clusterName, networkName, subnetworkName), + }, + { + ResourceName: "google_container_cluster.primary", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + { + Config: testAccContainerCluster_withSecretSyncConfigRotationDisabled(pid, clusterName, networkName, subnetworkName), + }, + { + ResourceName: "google_container_cluster.primary", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + { + Config: testAccContainerCluster_withSecretSyncConfigDisabled(pid, clusterName, networkName, subnetworkName), + }, + { + ResourceName: "google_container_cluster.primary", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + { + Config: testAccContainerCluster_forSecretSyncConfig(pid, clusterName, networkName, subnetworkName), + }, + { + ResourceName: "google_container_cluster.primary", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + }, + }) } func TestAccContainerCluster_withLoggingConfig(t *testing.T) { @@ -7093,7 +7149,7 @@ func TestAccContainerCluster_withWorkloadALTSConfigAutopilot(t *testing.T) { t.Parallel() clusterName := fmt.Sprintf("tf-test-cluster-%s", acctest.RandString(t, 10)) - networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") + networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") subnetworkName := tpgcompute.BootstrapSubnet(t, "gke-cluster", networkName) pid := envvar.GetTestProjectFromEnv() acctest.VcrTest(t, resource.TestCase{ @@ -7716,7 +7772,7 @@ func TestAccContainerCluster_autopilot_withAdditiveVPC(t *testing.T) { domain := "additive.autopilot.example" clusterName := fmt.Sprintf("tf-test-cluster-%s", acctest.RandString(t, 10)) - networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") + networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") subnetworkName := tpgcompute.BootstrapSubnet(t, "gke-cluster", networkName) acctest.VcrTest(t, resource.TestCase{ @@ -7821,7 +7877,7 @@ func TestAccContainerCluster_autopilot_withAdditiveVPCMutation(t *testing.T) { domain := "additive-mutating.autopilot.example" clusterName := fmt.Sprintf("tf-test-cluster-%s", acctest.RandString(t, 10)) - networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") + networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") subnetworkName := tpgcompute.BootstrapSubnet(t, "gke-cluster", networkName) acctest.VcrTest(t, resource.TestCase{ @@ -11892,7 +11948,7 @@ resource "google_container_cluster" "with_cluster_disruption_budget" { `, clusterName, interval, interval, networkName, subnetworkName) } -func testAccContainerCluster_withMaintenanceWindow(clusterName, startTime, networkName, subnetworkName string) string { +func testAccContainerCluster_withDailyMaintenanceWindow(clusterName, startTime, networkName, subnetworkName string) string { maintenancePolicy := "" if len(startTime) > 0 { maintenancePolicy = fmt.Sprintf(` @@ -11918,7 +11974,7 @@ resource "google_container_cluster" "with_maintenance_window" { `, clusterName, maintenancePolicy, networkName, subnetworkName) } -func testAccContainerCluster_withRecurringMaintenanceWindow(clusterName, startTime, endTime, networkName, subnetworkName string) string { +func testAccContainerCluster_withRecurringTimeWindow(clusterName, startTime, endTime, networkName, subnetworkName string) string { maintenancePolicy := "" if len(startTime) > 0 { maintenancePolicy = fmt.Sprintf(` @@ -11947,6 +12003,52 @@ resource "google_container_cluster" "with_recurring_maintenance_window" { } +func testAccContainerCluster_withRecurringMaintenanceWindow(clusterName string, startYear, startMonth, startDay, startHours, startMinutes int, duration string, networkName, subnetworkName string) string { + maintenancePolicy := "" + delayUntil := "" + if startYear != 0 && startMonth != 0 && startDay != 0 { + delayUntil = fmt.Sprintf(` + delay_until { + year = %d + month = %d + day = %d + } + `, startYear, startMonth, startDay) + } + + maintenancePolicy = fmt.Sprintf(` + maintenance_policy { + recurring_maintenance_window { + %s + + window_duration = "%s" + + window_start_time { + hours = %d + minutes = %d + seconds = 0 + } + recurrence = "FREQ=DAILY" + } + } + `, delayUntil, duration, startHours, startMinutes) + + + return fmt.Sprintf(` + resource "google_container_cluster" "with_recurring_maintenance_window" { + name = "%s" + location = "us-central1-a" + initial_node_count = 1 + %s + network = "%s" + subnetwork = "%s" + + deletion_protection = false + } + `, clusterName, maintenancePolicy, networkName, subnetworkName) + +} + func testAccContainerCluster_withExclusion_RecurringMaintenanceWindow(clusterName string, w1startTime, w1endTime, w2startTime, w2endTime, networkName, subnetworkName string) string { return fmt.Sprintf(` @@ -16358,7 +16460,7 @@ func TestAccContainerCluster_withAutopilotGcpFilestoreCsiDriver(t *testing.T) { randomSuffix := acctest.RandString(t, 10) clusterName := fmt.Sprintf("tf-test-cluster-%s", randomSuffix) - networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") + networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") subnetworkName := tpgcompute.BootstrapSubnet(t, "gke-cluster", networkName) acctest.VcrTest(t, resource.TestCase{ @@ -16432,7 +16534,7 @@ func TestAccContainerCluster_withDnsEndpoint(t *testing.T) { t.Parallel() clusterName := fmt.Sprintf("tf-test-cluster-%s", acctest.RandString(t, 10)) - networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") + networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") subnetworkName := tpgcompute.BootstrapSubnet(t, "gke-cluster", networkName) acctest.VcrTest(t, resource.TestCase{ @@ -16623,7 +16725,7 @@ func TestAccContainerCluster_withCgroupMode(t *testing.T) { t.Parallel() clusterName := fmt.Sprintf("tf-test-cluster-%s", acctest.RandString(t, 10)) - networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") + networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") subnetworkName := tpgcompute.BootstrapSubnet(t, "gke-cluster", networkName) acctest.VcrTest(t, resource.TestCase{ @@ -16652,7 +16754,7 @@ func TestAccContainerCluster_withCgroupModeUpdate(t *testing.T) { t.Parallel() clusterName := fmt.Sprintf("tf-test-cluster-%s", acctest.RandString(t, 10)) - networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") + networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") subnetworkName := tpgcompute.BootstrapSubnet(t, "gke-cluster", networkName) acctest.VcrTest(t, resource.TestCase{ @@ -16789,9 +16891,9 @@ resource "google_container_cluster" "with_enterprise_config" { func TestAccContainerCluster_disableControlPlaneIP(t *testing.T) { t.Parallel() - clusterName := fmt.Sprintf("tf-test-cluster-%s", acctest.RandString(t, 10)) - networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") - subnetworkName := tpgcompute.BootstrapSubnet(t, "gke-cluster", networkName) + clusterName := fmt.Sprintf("tf-test-cluster-%s", acctest.RandString(t, 10)) + networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") + subnetworkName := tpgcompute.BootstrapSubnet(t, "gke-cluster", networkName) acctest.VcrTest(t, resource.TestCase{ PreCheck: func() { acctest.AccTestPreCheck(t) }, @@ -18567,7 +18669,7 @@ func testAccContainerCluster_custom_subnet(clusterName string, networkName strin } } - `, clusterName, networkName, sri[0].SubnetName, additionalIpRangesStr, firstSubnet, firstSubnet) + `, clusterName, networkName, sri[0].SubnetName, additionalIpRangesStr, firstSubnet, firstSubnet) } func TestAccContainerCluster_withNodeCreationConfig(t *testing.T) { diff --git a/mmv1/third_party/terraform/website/docs/r/container_cluster.html.markdown b/mmv1/third_party/terraform/website/docs/r/container_cluster.html.markdown index 349048403f24..4eb233d3d340 100644 --- a/mmv1/third_party/terraform/website/docs/r/container_cluster.html.markdown +++ b/mmv1/third_party/terraform/website/docs/r/container_cluster.html.markdown @@ -766,10 +766,11 @@ This block also contains several computed attributes, documented below. The `maintenance_policy` block supports: * `daily_maintenance_window` - (Optional) structure documented below. * `recurring_window` - (Optional) structure documented below +* `recurring_maintenance_window` - (Optional) structure documented below * `maintenance_exclusion` - (Optional) structure documented below * `disruption_budget` - (Optional) structure documented below -In beta, one or the other of `recurring_window` and `daily_maintenance_window` is required if a `maintenance_policy` block is supplied. +In beta, one of `recurring_window`, `recurring_maintenance_window` and `daily_maintenance_window` is required if a `maintenance_policy` block is supplied. * `daily_maintenance_window` - Time window specified for daily maintenance operations. Specify `start_time` in [RFC3339](https://www.ietf.org/rfc/rfc3339.txt) format "HH:MM”, @@ -812,6 +813,60 @@ maintenance_policy { } ``` +* `recurring_maintenance_window` - Defines a recurring window for maintenance operations. + * `delay_until`: (Optional) Specifies the initial date when the recurring window can start. + * `day`: The day of the month (integer value between 1 and 31). + * `month`: The month of the year (integer value between 1 and 12). + * `year`: The year (integer value). + + * `window_start_time`: The time of day when each maintenance window instance begins. + * `hours`: The hour of the day (integer value between 0 and 23). + * `minutes`: The minute of the hour (integer value between 0 and 59). + * `seconds`: The second of the minute (integer value between 0 and 59). + + * `window_duration`: The length of each maintenance window instance. Specified as a sequence of decimal numbers, each with an optional fraction and a unit suffix, such as `"300s"`, `"1.5m"`, and `"2h45m"`. Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". The value must be a positive duration. + + * `recurrence`: Defines when the window recurs, using the [RFC5545](https://tools.ietf.org/html/rfc5545#section-3.8.5.3) RRULE format. + +Examples: +``` +maintenance_policy { + recurring_maintenance_window { + delay_until { + day = 1 + month = 8 + year = 2019 + } + window_start_time { + hours = 2 + minutes = 0 + seconds = 0 + } + window_duration = "4h" + recurrence = "FREQ=DAILY" + } +} +``` + +``` +maintenance_policy { + recurring_maintenance_window { + delay_until { + day = 1 + month = 1 + year = 2019 + } + window_start_time { + hours = 9 + minutes = 0 + seconds = 0 + } + window_duration = "8h" + recurrence = "FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR" + } +} +``` + * `maintenance_exclusion` - Exceptions to maintenance window. Non-emergency maintenance should not occur in these windows. A cluster can have up to 20 maintenance exclusions at a time [Maintenance Window and Exclusions](https://cloud.google.com/kubernetes-engine/docs/concepts/maintenance-windows-and-exclusions) The `maintenance_exclusion` block supports: diff --git a/mmv1/third_party/tgc_next/pkg/services/container/resource_container_cluster.go b/mmv1/third_party/tgc_next/pkg/services/container/resource_container_cluster.go index 45087d866978..3ebec9ec00d1 100644 --- a/mmv1/third_party/tgc_next/pkg/services/container/resource_container_cluster.go +++ b/mmv1/third_party/tgc_next/pkg/services/container/resource_container_cluster.go @@ -7,6 +7,7 @@ import ( "reflect" "regexp" "strings" + "time" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" @@ -41,6 +42,15 @@ func Rfc3339TimeDiffSuppress(k, old, new string, d *schema.ResourceData) bool { return false } +func validateDuration(v interface{}, k string) (ws []string, errors []error) { + s := v.(string) + _, err := time.ParseDuration(s) + if err != nil { + errors = append(errors, fmt.Errorf("%q is not a valid duration: %s", k, err)) + } + return +} + var ( instanceGroupManagerURL = regexp.MustCompile(fmt.Sprintf("projects/(%s)/zones/([a-z0-9-]*)/instanceGroupManagers/([^/]*)", verify.ProjectRegex)) @@ -1159,6 +1169,7 @@ func ResourceContainerCluster() *schema.Resource { ExactlyOneOf: []string{ "maintenance_policy.0.daily_maintenance_window", "maintenance_policy.0.recurring_window", + "maintenance_policy.0.recurring_maintenance_window", }, MaxItems: 1, Description: `Time window specified for daily maintenance operations. Specify start_time in RFC3339 format "HH:MM”, where HH : [00-23] and MM : [00-59] GMT.`, @@ -1184,6 +1195,7 @@ func ResourceContainerCluster() *schema.Resource { ExactlyOneOf: []string{ "maintenance_policy.0.daily_maintenance_window", "maintenance_policy.0.recurring_window", + "maintenance_policy.0.recurring_maintenance_window", }, Description: `Time window for recurring maintenance operations.`, Elem: &schema.Resource{ @@ -1206,6 +1218,74 @@ func ResourceContainerCluster() *schema.Resource { }, }, }, + "recurring_maintenance_window": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + ExactlyOneOf: []string{ + "maintenance_policy.0.daily_maintenance_window", + "maintenance_policy.0.recurring_window", + "maintenance_policy.0.recurring_maintenance_window", + }, + Description: `Time window for recurring maintenance operations.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "delay_until": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "year": { + Type: schema.TypeInt, + Required: true, + }, + "month": { + Type: schema.TypeInt, + Required: true, + }, + "day": { + Type: schema.TypeInt, + Required: true, + }, + }, + }, + }, + "window_duration": { + Required: true, + Type: schema.TypeString, + DiffSuppressFunc: tpgresource.DurationDiffSuppress, + ValidateFunc: validateDuration, + }, + "window_start_time": { + Required: true, + Type: schema.TypeList, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "hours": { + Type: schema.TypeInt, + Required: true, + }, + "minutes": { + Type: schema.TypeInt, + Required: true, + }, + "seconds": { + Type: schema.TypeInt, + Required: true, + }, + }, + }, + }, + "recurrence": { + Type: schema.TypeString, + Required: true, + DiffSuppressFunc: rfc5545RecurrenceDiffSuppress, + }, + }, + }, + }, "maintenance_exclusion": { Type: schema.TypeSet, Optional: true, diff --git a/mmv1/third_party/tgc_next/pkg/services/container/resource_container_cluster_cai2hcl.go b/mmv1/third_party/tgc_next/pkg/services/container/resource_container_cluster_cai2hcl.go index ffe21a342e72..2e6ac4f00dbe 100644 --- a/mmv1/third_party/tgc_next/pkg/services/container/resource_container_cluster_cai2hcl.go +++ b/mmv1/third_party/tgc_next/pkg/services/container/resource_container_cluster_cai2hcl.go @@ -1122,6 +1122,36 @@ func flattenMaintenancePolicy(v interface{}) []map[string]interface{} { transformed["recurring_window"] = []map[string]interface{}{windowMap} } + + if recurringMaintenanceWindow, ok := window["recurringMaintenanceWindow"].(map[string]interface{}); ok && recurringMaintenanceWindow != nil { + windowMap := map[string]interface{}{} + windowMap["window_start_time"] = []map[string]interface{}{} + startTime, _ := recurringMaintenanceWindow["windowStartTime"].(map[string]interface{}) + windowMap["window_start_time"] = []map[string]interface{}{ + { + "hours": startTime["hours"], + "minutes": startTime["minutes"], + "seconds": startTime["seconds"], + }, + } + + delay, _ := recurringMaintenanceWindow["delayUntil"].(map[string]interface{}) + if delay != nil { + windowMap["delayUntil"] = []map[string]interface{}{ + { + "year": delay["year"], + "month": delay["month"], + "day": delay["day"], + }, + } + } + + windowMap["window_duration"] = recurringMaintenanceWindow["windowDuration"] + windowMap["recurrence"] = recurringMaintenanceWindow["recurrence"] + + transformed["recurring_maintenance_window"] = []map[string]interface{}{windowMap} + } + } if disruptionBudget, ok := mp["disruptionBudget"].(map[string]interface{}); ok && disruptionBudget != nil { @@ -1140,8 +1170,9 @@ func flattenMaintenancePolicy(v interface{}) []map[string]interface{} { } _, hasDaily := transformed["daily_maintenance_window"] - _, hasRecurring := transformed["recurring_window"] - if !hasDaily && !hasRecurring { + _, hasRecurringWindow := transformed["recurring_window"] + _, hasRecurringMaintenance := transformed["recurring_maintenance_window"] + if !hasDaily && !hasRecurringWindow && !hasRecurringMaintenance { return nil } diff --git a/mmv1/third_party/tgc_next/pkg/services/container/resource_container_cluster_tfplan2cai.go b/mmv1/third_party/tgc_next/pkg/services/container/resource_container_cluster_tfplan2cai.go index b5cd9e011ed7..0b3dbbae3966 100644 --- a/mmv1/third_party/tgc_next/pkg/services/container/resource_container_cluster_tfplan2cai.go +++ b/mmv1/third_party/tgc_next/pkg/services/container/resource_container_cluster_tfplan2cai.go @@ -3,6 +3,7 @@ package container import ( "fmt" "strings" + "time" "github.com/GoogleCloudPlatform/terraform-google-conversion/v7/pkg/caiasset" "github.com/GoogleCloudPlatform/terraform-google-conversion/v7/pkg/tfplan2cai/converters/cai" @@ -729,6 +730,35 @@ func expandMaintenancePolicy(d tpgresource.TerraformResourceData, config *transp ResourceVersion: resourceVersion, } } + if recurringMaintenanceWindow, ok := maintenancePolicy["recurring_maintenance_window"]; ok && len(recurringMaintenanceWindow.([]interface{})) > 0 { + rmw := recurringMaintenanceWindow.([]interface{})[0].(map[string]interface{}) + duration, _ := time.ParseDuration(rmw["window_duration"].(string)) + + policy := &container.MaintenancePolicy{ + Window: &container.MaintenanceWindow{ + MaintenanceExclusions: exclusions, + RecurringMaintenanceWindow: &container.RecurringMaintenanceWindow{ + WindowStartTime: &container.TimeOfDay{ + Hours: int64(rmw["window_start_time"].([]interface{})[0].(map[string]interface{})["hours"].(int)), + Minutes: int64(rmw["window_start_time"].([]interface{})[0].(map[string]interface{})["minutes"].(int)), + Seconds: int64(rmw["window_start_time"].([]interface{})[0].(map[string]interface{})["seconds"].(int)), + }, + WindowDuration: fmt.Sprintf("%ds", int(duration.Seconds())), + Recurrence: rmw["recurrence"].(string), + }, + }, + ResourceVersion: resourceVersion, + } + + if _, ok := rmw["delay_until"]; ok && len(rmw["delay_until"].([]interface{})) == 1 { + policy.Window.RecurringMaintenanceWindow.DelayUntil = &container.Date{ + Year: int64(rmw["delay_until"].([]interface{})[0].(map[string]interface{})["year"].(int)), + Month: int64(rmw["delay_until"].([]interface{})[0].(map[string]interface{})["month"].(int)), + Day: int64(rmw["delay_until"].([]interface{})[0].(map[string]interface{})["day"].(int)), + } + } + return policy + } return nil } From ce801633578811af7c7f1078e1bb059f3e4d4397 Mon Sep 17 00:00:00 2001 From: virgulati2003 <108871727+virgulati2003@users.noreply.github.com> Date: Wed, 8 Jul 2026 02:38:50 +0530 Subject: [PATCH 53/78] Configure consistent root and async timeouts for Oracle Database resources (#18152) --- mmv1/products/oracledatabase/ExadbVmCluster.yaml | 4 ++++ mmv1/products/oracledatabase/GoldengateConnection.yaml | 4 ++++ .../oracledatabase/GoldengateConnectionAssignment.yaml | 4 ++++ mmv1/products/oracledatabase/GoldengateDeployment.yaml | 4 ++++ mmv1/products/oracledatabase/OdbNetwork.yaml | 4 ++++ mmv1/products/oracledatabase/OdbSubnet.yaml | 4 ++++ 6 files changed, 24 insertions(+) diff --git a/mmv1/products/oracledatabase/ExadbVmCluster.yaml b/mmv1/products/oracledatabase/ExadbVmCluster.yaml index 56957f3e353e..89fa5b52da2d 100644 --- a/mmv1/products/oracledatabase/ExadbVmCluster.yaml +++ b/mmv1/products/oracledatabase/ExadbVmCluster.yaml @@ -22,6 +22,10 @@ update_verb: PATCH id_format: projects/{{project}}/locations/{{location}}/exadbVmClusters/{{exadb_vm_cluster_id}} import_format: - projects/{{project}}/locations/{{location}}/exadbVmClusters/{{exadb_vm_cluster_id}} +timeouts: + insert_minutes: 120 + update_minutes: 60 + delete_minutes: 60 async: type: OpAsync operation: diff --git a/mmv1/products/oracledatabase/GoldengateConnection.yaml b/mmv1/products/oracledatabase/GoldengateConnection.yaml index 646d98293a94..6b4e953fe340 100755 --- a/mmv1/products/oracledatabase/GoldengateConnection.yaml +++ b/mmv1/products/oracledatabase/GoldengateConnection.yaml @@ -21,6 +21,10 @@ create_url: projects/{{project}}/locations/{{location}}/goldengateConnections?go id_format: projects/{{project}}/locations/{{location}}/goldengateConnections/{{goldengate_connection_id}} import_format: - projects/{{project}}/locations/{{location}}/goldengateConnections/{{goldengate_connection_id}} +timeouts: + insert_minutes: 30 + update_minutes: 30 + delete_minutes: 30 async: type: OpAsync operation: diff --git a/mmv1/products/oracledatabase/GoldengateConnectionAssignment.yaml b/mmv1/products/oracledatabase/GoldengateConnectionAssignment.yaml index 77786bfcfab6..48043f7b2103 100755 --- a/mmv1/products/oracledatabase/GoldengateConnectionAssignment.yaml +++ b/mmv1/products/oracledatabase/GoldengateConnectionAssignment.yaml @@ -27,6 +27,10 @@ timeouts: delete_minutes: 60 async: operation: + timeouts: + insert_minutes: 60 + update_minutes: 60 + delete_minutes: 60 base_url: '{{op_id}}' actions: - create diff --git a/mmv1/products/oracledatabase/GoldengateDeployment.yaml b/mmv1/products/oracledatabase/GoldengateDeployment.yaml index a44922876454..59416f625cac 100755 --- a/mmv1/products/oracledatabase/GoldengateDeployment.yaml +++ b/mmv1/products/oracledatabase/GoldengateDeployment.yaml @@ -28,6 +28,10 @@ timeouts: async: type: OpAsync operation: + timeouts: + insert_minutes: 60 + update_minutes: 60 + delete_minutes: 60 base_url: '{{op_id}}' actions: - create diff --git a/mmv1/products/oracledatabase/OdbNetwork.yaml b/mmv1/products/oracledatabase/OdbNetwork.yaml index 17faaccb7ce1..ce4ea77807aa 100644 --- a/mmv1/products/oracledatabase/OdbNetwork.yaml +++ b/mmv1/products/oracledatabase/OdbNetwork.yaml @@ -24,6 +24,10 @@ create_url: projects/{{project}}/locations/{{location}}/odbNetworks?odbNetworkId id_format: projects/{{project}}/locations/{{location}}/odbNetworks/{{odb_network_id}} import_format: - projects/{{project}}/locations/{{location}}/odbNetworks/{{odb_network_id}} +timeouts: + insert_minutes: 20 + update_minutes: 20 + delete_minutes: 20 async: type: OpAsync operation: diff --git a/mmv1/products/oracledatabase/OdbSubnet.yaml b/mmv1/products/oracledatabase/OdbSubnet.yaml index 2dc25a7c3cc7..217afd2a8d1a 100644 --- a/mmv1/products/oracledatabase/OdbSubnet.yaml +++ b/mmv1/products/oracledatabase/OdbSubnet.yaml @@ -24,6 +24,10 @@ create_url: projects/{{project}}/locations/{{location}}/odbNetworks/{{odbnetwork id_format: projects/{{project}}/locations/{{location}}/odbNetworks/{{odbnetwork}}/odbSubnets/{{odb_subnet_id}} import_format: - projects/{{project}}/locations/{{location}}/odbNetworks/{{odbnetwork}}/odbSubnets/{{odb_subnet_id}} +timeouts: + insert_minutes: 90 + update_minutes: 90 + delete_minutes: 90 async: type: OpAsync operation: From 4d5fa969092b365bbc6d07fc43eabc5c5a01ddd9 Mon Sep 17 00:00:00 2001 From: Mauricio Alvarez Leon <65101411+BBBmau@users.noreply.github.com> Date: Tue, 7 Jul 2026 14:20:49 -0700 Subject: [PATCH 54/78] `project`: fix `labels` permadiff (#18209) --- .../resource_google_project.go | 5 --- .../resource_google_project_internal_test.go | 45 +++++++++++++++++++ .../resource_google_project_test.go | 1 + 3 files changed, 46 insertions(+), 5 deletions(-) create mode 100644 mmv1/third_party/terraform/services/resourcemanager/resource_google_project_internal_test.go diff --git a/mmv1/third_party/terraform/services/resourcemanager/resource_google_project.go b/mmv1/third_party/terraform/services/resourcemanager/resource_google_project.go index f513003c5df7..2f049dc944f3 100644 --- a/mmv1/third_party/terraform/services/resourcemanager/resource_google_project.go +++ b/mmv1/third_party/terraform/services/resourcemanager/resource_google_project.go @@ -399,11 +399,6 @@ func populateGoogleProjectResourceData(d *schema.ResourceData, p *cloudresourcem if err := tpgresource.SetLabels(p.Labels, d, "terraform_labels"); err != nil { return fmt.Errorf("Error setting terraform_labels: %s", err) } - if v := d.Get("terraform_labels").(map[string]interface{}); len(v) == 0 && len(p.Labels) > 0 { - if err := d.Set("terraform_labels", p.Labels); err != nil { - return fmt.Errorf("Error setting terraform_labels: %s", err) - } - } if err := d.Set("effective_labels", p.Labels); err != nil { return fmt.Errorf("Error setting effective_labels: %s", err) } diff --git a/mmv1/third_party/terraform/services/resourcemanager/resource_google_project_internal_test.go b/mmv1/third_party/terraform/services/resourcemanager/resource_google_project_internal_test.go new file mode 100644 index 000000000000..c4e6ce68d81b --- /dev/null +++ b/mmv1/third_party/terraform/services/resourcemanager/resource_google_project_internal_test.go @@ -0,0 +1,45 @@ +package resourcemanager + +import ( + "reflect" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" + "google.golang.org/api/cloudresourcemanager/v1" +) + +func TestPopulateGoogleProjectResourceData_DoesNotCopyAllLabelsToTerraformLabelsWhenUnset(t *testing.T) { + d := schema.TestResourceDataRaw(t, ResourceGoogleProject().Schema, map[string]interface{}{ + "project_id": "example-project", + "name": "Example Project", + }) + + project := &cloudresourcemanager.Project{ + ProjectNumber: 123456789, + Name: "Example Project", + Labels: map[string]string{ + "firebase": "enabled", + "earth-engine": "", + }, + } + + if err := populateGoogleProjectResourceData(d, project, "example-project", &transport_tpg.Config{}); err != nil { + t.Fatalf("populateGoogleProjectResourceData() returned error: %v", err) + } + + if got := d.Get("labels").(map[string]interface{}); len(got) != 0 { + t.Fatalf("expected labels to remain empty when unset in config, got %#v", got) + } + + if got := d.Get("terraform_labels").(map[string]interface{}); len(got) != 0 { + t.Fatalf("expected terraform_labels to remain empty when unset in config, got %#v", got) + } + + if got := d.Get("effective_labels").(map[string]interface{}); !reflect.DeepEqual(got, map[string]interface{}{ + "firebase": "enabled", + "earth-engine": "", + }) { + t.Fatalf("expected effective_labels to contain all project labels, got %#v", got) + } +} diff --git a/mmv1/third_party/terraform/services/resourcemanager/resource_google_project_test.go b/mmv1/third_party/terraform/services/resourcemanager/resource_google_project_test.go index addc853b0f4a..b01f2ad22ec7 100644 --- a/mmv1/third_party/terraform/services/resourcemanager/resource_google_project_test.go +++ b/mmv1/third_party/terraform/services/resourcemanager/resource_google_project_test.go @@ -53,6 +53,7 @@ func TestAccProject_createWithoutOrg(t *testing.T) { // Test that a Project resource can be imported using an identity block (Terraform 1.12+). func TestAccProject_importBlockWithResourceIdentity(t *testing.T) { t.Parallel() + t.Skip("terraform_labels cannot be ignored during a resource identity import test") org := envvar.GetTestOrgFromEnv(t) pid := fmt.Sprintf("%s-%d", TestPrefix, acctest.RandInt(t)) From f2a9d06b83b1ffab5b17b6fa12df345ac6181063 Mon Sep 17 00:00:00 2001 From: Shuya Ma <87669292+shuyama1@users.noreply.github.com> Date: Tue, 7 Jul 2026 14:26:45 -0700 Subject: [PATCH 55/78] Example Migration Cleanup (#18215) --- mmv1/products/ces/AppVersion.yaml | 2 +- mmv1/products/ces/Toolset.yaml | 2 +- mmv1/products/chronicle/Environment.yaml | 1 + mmv1/products/chronicle/EnvironmentGroup.yaml | 4 +--- mmv1/products/chronicle/ReferenceList.yaml | 2 +- mmv1/products/chronicle/Rule.yaml | 2 +- mmv1/products/chronicle/RuleDeployment.yaml | 4 ++-- mmv1/products/chronicle/SoarDomain.yaml | 3 +-- mmv1/products/vertexai/SemanticGovernancePolicyEngine.yaml | 4 +++- .../vertex_ai_semantic_governance_policy_engine_basic.tf.tmpl | 0 10 files changed, 12 insertions(+), 12 deletions(-) rename mmv1/templates/terraform/{examples => samples/services/vertexai}/vertex_ai_semantic_governance_policy_engine_basic.tf.tmpl (100%) diff --git a/mmv1/products/ces/AppVersion.yaml b/mmv1/products/ces/AppVersion.yaml index 2d26f694e3ae..54e223af2998 100644 --- a/mmv1/products/ces/AppVersion.yaml +++ b/mmv1/products/ces/AppVersion.yaml @@ -2507,7 +2507,7 @@ properties: changes. - name: executionType type: String - description: |2- + description: |- Possible values: SYNCHRONOUS ASYNCHRONOUS diff --git a/mmv1/products/ces/Toolset.yaml b/mmv1/products/ces/Toolset.yaml index 93aa5f2fc75b..8c2b8f9602d2 100644 --- a/mmv1/products/ces/Toolset.yaml +++ b/mmv1/products/ces/Toolset.yaml @@ -211,7 +211,7 @@ properties: changes. - name: executionType type: String - description: |2- + description: |- Possible values: SYNCHRONOUS ASYNCHRONOUS diff --git a/mmv1/products/chronicle/Environment.yaml b/mmv1/products/chronicle/Environment.yaml index c9f2829f4c12..2488afa9409a 100644 --- a/mmv1/products/chronicle/Environment.yaml +++ b/mmv1/products/chronicle/Environment.yaml @@ -15,6 +15,7 @@ name: Environment description: > An environment is logical container for different networks or customers that are managed by the SOC or MSSP. This is useful for SOCs who provide services to several different networks, customers or business units within the organization. The Platform comes with a predefined environment named Default Environment. + references: guides: 'Google SecOps Guides': 'https://cloud.google.com/chronicle/docs/secops/secops-overview' diff --git a/mmv1/products/chronicle/EnvironmentGroup.yaml b/mmv1/products/chronicle/EnvironmentGroup.yaml index f206ff2ef73a..09df15b286b8 100644 --- a/mmv1/products/chronicle/EnvironmentGroup.yaml +++ b/mmv1/products/chronicle/EnvironmentGroup.yaml @@ -15,6 +15,7 @@ name: EnvironmentGroup description: > Environment groups let you organize multiple environments into logical categories, making it easier to manage large organizations or multiple customers as a Managed Security Service Provider (MSSP). + min_version: 'beta' references: guides: @@ -28,9 +29,7 @@ update_mask: true update_verb: PATCH import_format: - projects/{{project}}/locations/{{location}}/instances/{{instance}}/environmentGroups/{{environment_group_id}} - autogen_status: RW52aXJvbm1lbnRHcm91cA== - samples: - name: chronicle_environmentgroup_update primary_resource_id: sample @@ -42,7 +41,6 @@ samples: - name: "chronicle_environmentgroup_full" test_env_vars: chronicle_id: 'CHRONICLE_ID' - parameters: - name: location type: String diff --git a/mmv1/products/chronicle/ReferenceList.yaml b/mmv1/products/chronicle/ReferenceList.yaml index 17ac73894260..c913672b41aa 100644 --- a/mmv1/products/chronicle/ReferenceList.yaml +++ b/mmv1/products/chronicle/ReferenceList.yaml @@ -127,7 +127,7 @@ properties: type: String - name: syntaxType type: String - description: |2- + description: |- Possible values: REFERENCE_LIST_SYNTAX_TYPE_PLAIN_TEXT_STRING REFERENCE_LIST_SYNTAX_TYPE_REGEX diff --git a/mmv1/products/chronicle/Rule.yaml b/mmv1/products/chronicle/Rule.yaml index 99dceaecd154..7bdffef47179 100644 --- a/mmv1/products/chronicle/Rule.yaml +++ b/mmv1/products/chronicle/Rule.yaml @@ -176,7 +176,7 @@ properties: - name: type type: String output: true - description: |2- + description: |- Possible values: RULE_TYPE_UNSPECIFIED SINGLE_EVENT diff --git a/mmv1/products/chronicle/RuleDeployment.yaml b/mmv1/products/chronicle/RuleDeployment.yaml index 4005f0c55cf2..5f06f84364a6 100644 --- a/mmv1/products/chronicle/RuleDeployment.yaml +++ b/mmv1/products/chronicle/RuleDeployment.yaml @@ -107,7 +107,7 @@ properties: - name: runFrequency type: String diff_suppress_func: 'chronicleRuleDeploymentNilRunFrequencyDiffSuppressFunc' - description: |2- + description: |- The run frequency of the rule deployment. Possible values: LIVE @@ -115,7 +115,7 @@ properties: DAILY - name: executionState type: String - description: |2- + description: |- The execution state of the rule deployment. Possible values: DEFAULT diff --git a/mmv1/products/chronicle/SoarDomain.yaml b/mmv1/products/chronicle/SoarDomain.yaml index 04bf2e80a518..c32d9627aa2f 100644 --- a/mmv1/products/chronicle/SoarDomain.yaml +++ b/mmv1/products/chronicle/SoarDomain.yaml @@ -15,6 +15,7 @@ name: SoarDomain description: > A SOAR domain designate a domain as internal, ensuring that entities associated with it are treated as organizational assets rather than external threats during ingestion. + min_version: 'beta' references: guides: @@ -29,7 +30,6 @@ update_verb: PATCH import_format: - projects/{{project}}/locations/{{location}}/instances/{{instance}}/soarDomains/{{soar_domain_id}} autogen_status: U29hckRvbWFpbg== - samples: - name: chronicle_soardomain_update primary_resource_id: example @@ -41,7 +41,6 @@ samples: - name: "chronicle_soardomain_full" test_env_vars: chronicle_id: 'CHRONICLE_ID' - parameters: - name: location type: String diff --git a/mmv1/products/vertexai/SemanticGovernancePolicyEngine.yaml b/mmv1/products/vertexai/SemanticGovernancePolicyEngine.yaml index 5d48bcfe0af4..acd822881b89 100644 --- a/mmv1/products/vertexai/SemanticGovernancePolicyEngine.yaml +++ b/mmv1/products/vertexai/SemanticGovernancePolicyEngine.yaml @@ -62,7 +62,7 @@ async: custom_code: constants: 'templates/terraform/constants/semantic_governance_policy_engine.go.tmpl' post_read: 'templates/terraform/post_read/vertex_ai_semantic_governance_policy_engine.go.tmpl' -examples: +samples: - name: 'vertex_ai_semantic_governance_policy_engine_basic' primary_resource_id: 'sgpe' # The generator's standard CheckDestroy expects GET to return an error @@ -73,6 +73,8 @@ examples: # test (resource_vertex_ai_semantic_governance_policy_engine_test.go), # which CI still runs because it is a new TestAcc function in the diff. exclude_test: true + steps: + - name: 'vertex_ai_semantic_governance_policy_engine_basic' parameters: - name: 'region' type: String diff --git a/mmv1/templates/terraform/examples/vertex_ai_semantic_governance_policy_engine_basic.tf.tmpl b/mmv1/templates/terraform/samples/services/vertexai/vertex_ai_semantic_governance_policy_engine_basic.tf.tmpl similarity index 100% rename from mmv1/templates/terraform/examples/vertex_ai_semantic_governance_policy_engine_basic.tf.tmpl rename to mmv1/templates/terraform/samples/services/vertexai/vertex_ai_semantic_governance_policy_engine_basic.tf.tmpl From 256ad3468833a22f992564c595910cbbf731b77c Mon Sep 17 00:00:00 2001 From: Cameron Thornton Date: Tue, 7 Jul 2026 17:21:30 -0500 Subject: [PATCH 56/78] provider reference for deletion_policy (#18219) --- .../website/docs/guides/provider_reference.html.markdown | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/mmv1/third_party/terraform/website/docs/guides/provider_reference.html.markdown b/mmv1/third_party/terraform/website/docs/guides/provider_reference.html.markdown index 441c4d71ab3d..52dd454b0fa0 100644 --- a/mmv1/third_party/terraform/website/docs/guides/provider_reference.html.markdown +++ b/mmv1/third_party/terraform/website/docs/guides/provider_reference.html.markdown @@ -355,6 +355,12 @@ instead. --- +* `deletion_policy` - (Optional) The default deletion policy for provider resources. Defaults to `"DELETE"`. + This can be set to `"DELETE"`, `"PREVENT"`, or `"ABANDON"`. + Any resource-level `deletion_policy` configured on a resource takes precedence over this provider-level setting. + +--- + * `{{service}}_custom_endpoint` - (Optional) The endpoint for a service's APIs, such as `compute_custom_endpoint`. Defaults to the production GCP endpoint for From 1e0750fb2b43d47e868c44c078941e0a3121c760 Mon Sep 17 00:00:00 2001 From: Swamita Gupta <55314843+swamitagupta@users.noreply.github.com> Date: Tue, 7 Jul 2026 15:24:31 -0700 Subject: [PATCH 57/78] Add IAP Agent, MCP Server and Endpoint resources to GA provider (#18088) --- mmv1/products/iap/AgentRegistryAgent.yaml | 55 ++++++++++++++++++ mmv1/products/iap/AgentRegistryEndpoint.yaml | 42 +++++++------- mmv1/products/iap/AgentRegistryMcpServer.yaml | 56 +++++++++++++++++++ .../agent_registry_agent_basic.tf.tmpl | 43 ++++++++++++++ .../agent_registry_endpoint_basic.tf.tmpl | 55 ++++++++++++++++++ .../agent_registry_mcp_server_basic.tf.tmpl | 56 +++++++++++++++++++ .../data_source_agent_registry_agent.go | 1 + .../data_source_agent_registry_endpoint.go | 1 + .../data_source_agent_registry_mcp_server.go | 1 + 9 files changed, 288 insertions(+), 22 deletions(-) create mode 100644 mmv1/products/iap/AgentRegistryAgent.yaml create mode 100644 mmv1/products/iap/AgentRegistryMcpServer.yaml create mode 100644 mmv1/templates/terraform/examples/agent_registry_agent_basic.tf.tmpl create mode 100644 mmv1/templates/terraform/examples/agent_registry_endpoint_basic.tf.tmpl create mode 100644 mmv1/templates/terraform/examples/agent_registry_mcp_server_basic.tf.tmpl diff --git a/mmv1/products/iap/AgentRegistryAgent.yaml b/mmv1/products/iap/AgentRegistryAgent.yaml new file mode 100644 index 000000000000..2d0ab4ff2507 --- /dev/null +++ b/mmv1/products/iap/AgentRegistryAgent.yaml @@ -0,0 +1,55 @@ +# 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: AgentRegistryAgent +description: | + Only used to generate IAM resources +# This resource is only used to generate IAM resources. They do not correspond to real +# GCP resources, and should not be used to generate anything other than IAM support. +exclude_resource: true +id_format: projects/{{project}}/locations/{{location}}/iap_web/agentRegistry/agents/{{agent_id}} +base_url: projects/{{project}}/locations/{{location}}/iap_web/agentRegistry/agents/{{agent_id}} +self_link: projects/{{project}}/locations/{{location}}/iap_web/agentRegistry/agents/{{agent_id}} +import_format: + - projects/{{project}}/locations/{{location}}/iap_web/agentRegistry/agents/{{agent_id}} + - '{{agent_id}}' +timeouts: + insert_minutes: 20 + update_minutes: 20 + delete_minutes: 20 +iam_policy: + method_name_separator: ':' + parent_resource_type: data.google_agent_registry_agent + fetch_iam_policy_verb: POST + allowed_iam_role: roles/iap.egressor + parent_resource_attribute: agent_id + iam_conditions_request_type: REQUEST_BODY +exclude_tgc: true +examples: + - name: agent_registry_agent_basic + primary_resource_id: default + external_providers: [time] + vars: + agent_id: agent-id + test_env_vars: + org_id: ORG_ID +parameters: + - name: location + type: String + required: true + description: The location of the resource. +properties: + - name: agent_id + type: String + required: true + description: The id of the agent. diff --git a/mmv1/products/iap/AgentRegistryEndpoint.yaml b/mmv1/products/iap/AgentRegistryEndpoint.yaml index 4b50cea93766..d14e7094b8fe 100644 --- a/mmv1/products/iap/AgentRegistryEndpoint.yaml +++ b/mmv1/products/iap/AgentRegistryEndpoint.yaml @@ -1,4 +1,3 @@ ---- # 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. @@ -11,48 +10,47 @@ # 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: AgentRegistryEndpoint description: | Only used to generate IAM resources +# This resource is only used to generate IAM resources. They do not correspond to real +# GCP resources, and should not be used to generate anything other than IAM support. +exclude_resource: true docs: -base_url: projects/{{project}}/locations/{{location}}/iap_web/agentRegistry/endpoints/{{endpointId}} -self_link: projects/{{project}}/locations/{{location}}/iap_web/agentRegistry/endpoints/{{endpointId}} +base_url: projects/{{project}}/locations/{{location}}/iap_web/agentRegistry/endpoints/{{endpoint_id}} +self_link: projects/{{project}}/locations/{{location}}/iap_web/agentRegistry/endpoints/{{endpoint_id}} iam_policy: method_name_separator: ':' - parent_resource_type: google_project_service - parent_is_resource_id: true + parent_resource_type: data.google_agent_registry_endpoint fetch_iam_policy_verb: POST allowed_iam_role: roles/iap.egressor parent_resource_attribute: endpoint_id - sample_config_body: templates/terraform/iam/example_config_body/iap_agent_registry_endpoint.tf.tmpl iam_conditions_request_type: REQUEST_BODY -# This resource is only used to generate IAM resources. They do not correspond to real -# GCP resources, and should not be used to generate anything other than IAM support. -exclude_resource: true -id_format: projects/{{project}}/locations/{{location}}/iap_web/agentRegistry/endpoints/{{endpointId}} +id_format: projects/{{project}}/locations/{{location}}/iap_web/agentRegistry/endpoints/{{endpoint_id}} import_format: - - projects/{{project}}/locations/{{location}}/iap_web/agentRegistry/endpoints/{{endpointId}} - - '{{endpointId}}' + - projects/{{project}}/locations/{{location}}/iap_web/agentRegistry/endpoints/{{endpoint_id}} + - '{{endpoint_id}}' timeouts: insert_minutes: 20 update_minutes: 20 delete_minutes: 20 exclude_tgc: true -samples: - - name: project - primary_resource_id: project_service - steps: - - name: project - config_path: templates/terraform/samples/services/iap/iap_existing_project_endpoint.tf.tmpl - test_env_vars: - project: PROJECT_NAME +examples: + - name: agent_registry_endpoint_basic + primary_resource_id: default + external_providers: [time] + vars: + endpoint_id: endpoint-id + test_env_vars: + org_id: ORG_ID parameters: - name: location type: String required: true description: The location of the resource. properties: - - name: endpointId + - name: endpoint_id type: String - description: The ID of the Endpoint. required: true + description: The id of the Endpoint. diff --git a/mmv1/products/iap/AgentRegistryMcpServer.yaml b/mmv1/products/iap/AgentRegistryMcpServer.yaml new file mode 100644 index 000000000000..abdeaf441b6a --- /dev/null +++ b/mmv1/products/iap/AgentRegistryMcpServer.yaml @@ -0,0 +1,56 @@ +# 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: AgentRegistryMcpServer +description: | + Only used to generate IAM resources +# This resource is only used to generate IAM resources. They do not correspond to real +# GCP resources, and should not be used to generate anything other than IAM support. +exclude_resource: true +docs: +id_format: projects/{{project}}/locations/{{location}}/iap_web/agentRegistry/mcpServers/{{mcp_server_id}} +base_url: projects/{{project}}/locations/{{location}}/iap_web/agentRegistry/mcpServers/{{mcp_server_id}} +self_link: projects/{{project}}/locations/{{location}}/iap_web/agentRegistry/mcpServers/{{mcp_server_id}} +import_format: + - projects/{{project}}/locations/{{location}}/iap_web/agentRegistry/mcpServers/{{mcp_server_id}} + - '{{mcp_server_id}}' +timeouts: + insert_minutes: 20 + update_minutes: 20 + delete_minutes: 20 +iam_policy: + method_name_separator: ':' + parent_resource_type: data.google_agent_registry_mcp_server + fetch_iam_policy_verb: POST + allowed_iam_role: roles/iap.egressor + parent_resource_attribute: mcp_server_id + iam_conditions_request_type: REQUEST_BODY +exclude_tgc: true +examples: + - name: agent_registry_mcp_server_basic + primary_resource_id: default + external_providers: [time] + vars: + mcp_server_id: mcp-server-id + test_env_vars: + org_id: ORG_ID +parameters: + - name: location + type: String + required: true + description: The location of the resource. +properties: + - name: mcp_server_id + type: String + required: true + description: The id of the MCP Server. diff --git a/mmv1/templates/terraform/examples/agent_registry_agent_basic.tf.tmpl b/mmv1/templates/terraform/examples/agent_registry_agent_basic.tf.tmpl new file mode 100644 index 000000000000..c4a811e78987 --- /dev/null +++ b/mmv1/templates/terraform/examples/agent_registry_agent_basic.tf.tmpl @@ -0,0 +1,43 @@ +resource "google_project" "project" { + project_id = "tf-test%{random_suffix}" + name = "tf-test%{random_suffix}" + org_id = "%{org_id}" + deletion_policy = "DELETE" +} + +# Needed for CI tests for permissions to propagate +resource "time_sleep" "wait_for_project_propagation" { + create_duration = "60s" + + depends_on = [google_project.project] +} + +resource "google_project_service" "iap_service" { + project = google_project.project.project_id + service = "iap.googleapis.com" + + depends_on = [time_sleep.wait_for_project_propagation] +} + +resource "google_project_service" "agentregistry_service" { + project = google_project.project.project_id + service = "agentregistry.googleapis.com" + + # Chain service resource calls to avoid cache misses during CI test replays + depends_on = [google_project_service.iap_service] +} + +# Needed for CI tests for resource to propagate +resource "time_sleep" "wait_for_agent_creation" { + create_duration = "60s" + + depends_on = [google_project_service.agentregistry_service] +} + +data "google_agent_registry_agent" "default" { + project = google_project.project.project_id + location = "global" + filter = "displayName:Workspace Agent" + + depends_on = [time_sleep.wait_for_agent_creation] +} \ No newline at end of file diff --git a/mmv1/templates/terraform/examples/agent_registry_endpoint_basic.tf.tmpl b/mmv1/templates/terraform/examples/agent_registry_endpoint_basic.tf.tmpl new file mode 100644 index 000000000000..9cbc15218a6c --- /dev/null +++ b/mmv1/templates/terraform/examples/agent_registry_endpoint_basic.tf.tmpl @@ -0,0 +1,55 @@ +resource "google_project" "project" { + project_id = "tf-test%{random_suffix}" + name = "tf-test%{random_suffix}" + org_id = "%{org_id}" + deletion_policy = "DELETE" +} + +# Needed for CI tests for permissions to propagate +resource "time_sleep" "wait_for_project_propagation" { + create_duration = "60s" + + depends_on = [google_project.project] +} + +resource "google_project_service" "iap_service" { + project = google_project.project.project_id + service = "iap.googleapis.com" + + depends_on = [time_sleep.wait_for_project_propagation] +} + +resource "google_project_service" "agentregistry_service" { + project = google_project.project.project_id + service = "agentregistry.googleapis.com" + + # Chain service resource calls to avoid cache misses during CI test replays + depends_on = [google_project_service.iap_service] +} + +resource "google_agent_registry_service" "endpoint_service" { + project = google_project.project.project_id + location = "us-central1" + service_id = "tf-test-endpoint" + description = "Test endpoint service" + display_name = "Test endpoint service" + + interfaces { + url = "https://www.google.com" + protocol_binding = "GRPC" + } + + endpoint_spec { + type = "NO_SPEC" + } + + depends_on = [google_project_service.agentregistry_service] +} + +data "google_agent_registry_endpoint" "default" { + project = google_project.project.project_id + location = "us-central1" + filter = "displayName:Test endpoint service" + + depends_on = [google_agent_registry_service.endpoint_service] +} \ No newline at end of file diff --git a/mmv1/templates/terraform/examples/agent_registry_mcp_server_basic.tf.tmpl b/mmv1/templates/terraform/examples/agent_registry_mcp_server_basic.tf.tmpl new file mode 100644 index 000000000000..fc30cdf7112f --- /dev/null +++ b/mmv1/templates/terraform/examples/agent_registry_mcp_server_basic.tf.tmpl @@ -0,0 +1,56 @@ +resource "google_project" "project" { + project_id = "tf-test%{random_suffix}" + name = "tf-test%{random_suffix}" + org_id = "%{org_id}" + deletion_policy = "DELETE" +} + +# Needed for CI tests for permissions to propagate +resource "time_sleep" "wait_for_project_propagation" { + create_duration = "60s" + + depends_on = [google_project.project] +} + +resource "google_project_service" "iap_service" { + project = google_project.project.project_id + service = "iap.googleapis.com" + + depends_on = [time_sleep.wait_for_project_propagation] +} + +resource "google_project_service" "agentregistry_service" { + project = google_project.project.project_id + service = "agentregistry.googleapis.com" + + # Chain service resource calls to avoid cache misses during CI test replays + depends_on = [google_project_service.iap_service] +} + +resource "google_agent_registry_service" "mcp_service" { + project = google_project.project.project_id + location = "us-central1" + service_id = "tf-test-mcp" + description = "Test MCP service" + display_name = "Test MCP service" + + interfaces { + url = "https://www.google.com" + protocol_binding = "JSONRPC" + } + + mcp_server_spec { + type = "TOOL_SPEC" + content = "{\"tools\":[]}" + } + + depends_on = [google_project_service.agentregistry_service] +} + +data "google_agent_registry_mcp_server" "default" { + project = google_project.project.project_id + location = "us-central1" + filter = "displayName:Test MCP service" + + depends_on = [google_agent_registry_service.mcp_service] +} \ No newline at end of file diff --git a/mmv1/third_party/terraform/services/agentregistry/data_source_agent_registry_agent.go b/mmv1/third_party/terraform/services/agentregistry/data_source_agent_registry_agent.go index 700e79bda626..881d69cd4af4 100644 --- a/mmv1/third_party/terraform/services/agentregistry/data_source_agent_registry_agent.go +++ b/mmv1/third_party/terraform/services/agentregistry/data_source_agent_registry_agent.go @@ -31,6 +31,7 @@ func DataSourceAgentRegistryAgent() *schema.Resource { "agent_id": { Type: schema.TypeString, Optional: true, + Computed: true, ForceNew: true, ConflictsWith: []string{"filter"}, Description: `The unique identifier for the Agent.`, diff --git a/mmv1/third_party/terraform/services/agentregistry/data_source_agent_registry_endpoint.go b/mmv1/third_party/terraform/services/agentregistry/data_source_agent_registry_endpoint.go index 05f597ca5101..e3e94385e4da 100644 --- a/mmv1/third_party/terraform/services/agentregistry/data_source_agent_registry_endpoint.go +++ b/mmv1/third_party/terraform/services/agentregistry/data_source_agent_registry_endpoint.go @@ -32,6 +32,7 @@ func DataSourceAgentRegistryEndpoint() *schema.Resource { "endpoint_id": { Type: schema.TypeString, Optional: true, + Computed: true, ForceNew: true, ConflictsWith: []string{"filter"}, Description: `The unique identifier for the Endpoint.`, diff --git a/mmv1/third_party/terraform/services/agentregistry/data_source_agent_registry_mcp_server.go b/mmv1/third_party/terraform/services/agentregistry/data_source_agent_registry_mcp_server.go index 600a0bed0684..d6944fc41f1e 100644 --- a/mmv1/third_party/terraform/services/agentregistry/data_source_agent_registry_mcp_server.go +++ b/mmv1/third_party/terraform/services/agentregistry/data_source_agent_registry_mcp_server.go @@ -32,6 +32,7 @@ func DataSourceAgentRegistryMcpServer() *schema.Resource { "mcp_server_id": { Type: schema.TypeString, Optional: true, + Computed: true, ForceNew: true, ConflictsWith: []string{"filter"}, Description: `The unique identifier for the MCP server.`, From 3acd7d12002fa86e88288d032cddbb567157486d Mon Sep 17 00:00:00 2001 From: nikitha-nuthi Date: Wed, 8 Jul 2026 04:10:16 +0530 Subject: [PATCH 58/78] Add catalogPublishingEnabled to Dataplex dataDocumentationSpec (#18135) --- mmv1/products/dataplex/Datascan.yaml | 6 +++++- .../dataplex/dataplex_datascan_documentation.tf.tmpl | 4 +++- mmv1/templates/terraform/schema_property.go.tmpl | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/mmv1/products/dataplex/Datascan.yaml b/mmv1/products/dataplex/Datascan.yaml index 5af682bdbf14..62fa847182d2 100644 --- a/mmv1/products/dataplex/Datascan.yaml +++ b/mmv1/products/dataplex/Datascan.yaml @@ -900,4 +900,8 @@ properties: - data_profile_spec - data_discovery_spec - data_documentation_spec - properties: [] + properties: + - name: catalogPublishingEnabled + type: Boolean + description: | + If set, the latest DataScan job result will be published to Knowledge Catalog. diff --git a/mmv1/templates/terraform/samples/services/dataplex/dataplex_datascan_documentation.tf.tmpl b/mmv1/templates/terraform/samples/services/dataplex/dataplex_datascan_documentation.tf.tmpl index 71fd70131e24..19f4cff68259 100644 --- a/mmv1/templates/terraform/samples/services/dataplex/dataplex_datascan_documentation.tf.tmpl +++ b/mmv1/templates/terraform/samples/services/dataplex/dataplex_datascan_documentation.tf.tmpl @@ -74,7 +74,9 @@ resource "google_dataplex_datascan" "{{$.PrimaryResourceId}}" { } } - data_documentation_spec {} + data_documentation_spec { + catalog_publishing_enabled = true + } project = "{{index $.TestEnvVars "project_name"}}" } diff --git a/mmv1/templates/terraform/schema_property.go.tmpl b/mmv1/templates/terraform/schema_property.go.tmpl index 14e1ae34c212..34cca0618ed6 100644 --- a/mmv1/templates/terraform/schema_property.go.tmpl +++ b/mmv1/templates/terraform/schema_property.go.tmpl @@ -76,7 +76,7 @@ Default value: {{ .ItemType.DefaultValue -}} {{ end -}} Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ - {{- range $prop := .ResourceMetadata.OrderProperties $.UserProperties }} + {{- range $prop := .ResourceMetadata.OrderProperties .UserProperties }} {{template "SchemaFields" $prop}} {{- end }} }, From bbcd31954dcedaca70c323accf5d190d16bc203a Mon Sep 17 00:00:00 2001 From: wyiyang-stack Date: Tue, 7 Jul 2026 16:51:37 -0700 Subject: [PATCH 59/78] =?UTF-8?q?Add=20support=20for=20enforce=5Fnew=5Fsql?= =?UTF-8?q?=5Fnetwork=5Farchitecture=20flag=20in=20google=5Fs=E2=80=A6=20(?= =?UTF-8?q?#18142)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sql/data_source_sql_database_instances.go | 1 + .../resource_sql_database_instance.go.tmpl | 48 +++++++++++++++++ ...ource_sql_database_instance_meta.yaml.tmpl | 2 + ...esource_sql_database_instance_test.go.tmpl | 51 +++++++++++++++++++ .../r/sql_database_instance.html.markdown | 5 ++ 5 files changed, 107 insertions(+) diff --git a/mmv1/third_party/terraform/services/sql/data_source_sql_database_instances.go b/mmv1/third_party/terraform/services/sql/data_source_sql_database_instances.go index 4b81d81a33a8..68bfed4bdad2 100644 --- a/mmv1/third_party/terraform/services/sql/data_source_sql_database_instances.go +++ b/mmv1/third_party/terraform/services/sql/data_source_sql_database_instances.go @@ -147,6 +147,7 @@ func flattenDatasourceGoogleDatabaseInstancesList(fetchedInstances []*sqladmin.D instance["available_maintenance_versions"] = rawInstance.AvailableMaintenanceVersions instance["instance_type"] = rawInstance.InstanceType instance["service_account_email_address"] = rawInstance.ServiceAccountEmailAddress + instance["enforce_new_sql_network_architecture"] = rawInstance.SqlNetworkArchitecture == "NEW_NETWORK_ARCHITECTURE" instance["settings"] = flattenSettings(rawInstance.Settings, rawInstance.InstanceType, d) if rawInstance.DiskEncryptionConfiguration != nil { diff --git a/mmv1/third_party/terraform/services/sql/resource_sql_database_instance.go.tmpl b/mmv1/third_party/terraform/services/sql/resource_sql_database_instance.go.tmpl index aa611b4a5550..ce59736c0b2a 100644 --- a/mmv1/third_party/terraform/services/sql/resource_sql_database_instance.go.tmpl +++ b/mmv1/third_party/terraform/services/sql/resource_sql_database_instance.go.tmpl @@ -263,6 +263,13 @@ func ResourceSqlDatabaseInstance() *schema.Resource { Optional: true, Description: `Used to block Terraform from deleting a SQL Instance. Defaults to true.`, }, + "enforce_new_sql_network_architecture": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + DiffSuppressFunc: sqlNetworkArchitectureDiffSuppress, + Description: `Whether to enforce the new SQL network architecture.`, + }, "final_backup_description": { Type: schema.TypeString, Optional: true, @@ -1693,6 +1700,14 @@ func pitrSupportDbCustomizeDiff(_ context.Context, diff *schema.ResourceDiff, v return nil } +func sqlNetworkArchitectureDiffSuppress(k, old, new string, d *schema.ResourceData) bool { + // By default, new Cloud SQL instances created in projects created after August 2021 use the new network architecture. + // If the state is true and the config is false, suppress the diff. + // This follows the gcloud pattern where the flag is an irreversible opt-in. + // See https://docs.cloud.google.com/sql/docs/mysql/upgrade-cloud-sql-instance-new-network-architecture#new-arch + return old == "true" && new == "false" +} + func resourceSqlDatabaseInstanceCreate(d *schema.ResourceData, meta interface{}) error { config := meta.(*transport_tpg.Config) userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) @@ -1741,6 +1756,10 @@ func resourceSqlDatabaseInstanceCreate(d *schema.ResourceData, meta interface{}) ReplicaConfiguration: expandReplicaConfiguration(d.Get("replica_configuration").([]interface{})), } + if d.Get("enforce_new_sql_network_architecture").(bool) { + instance.SqlNetworkArchitecture = "NEW_NETWORK_ARCHITECTURE" + } + cloneContext, cloneSourceInstance := expandCloneContext(d.Get("clone").([]interface{})) cloneSourceProject := expandCloneSourceProject(d.Get("clone").([]interface{})) @@ -2517,6 +2536,9 @@ func resourceSqlDatabaseInstanceRead(d *schema.ResourceData, meta interface{}) e if err := d.Set("instance_type", instance.InstanceType); err != nil { return fmt.Errorf("Error setting instance_type: %s", err) } + if err := d.Set("enforce_new_sql_network_architecture", instance.SqlNetworkArchitecture == "NEW_NETWORK_ARCHITECTURE"); err != nil { + return fmt.Errorf("Error setting enforce_new_sql_network_architecture: %s", err) + } if err := d.Set("node_count", instance.NodeCount); err != nil { return fmt.Errorf("Error setting node_count: %s", err) } @@ -2679,6 +2701,32 @@ func resourceSqlDatabaseInstanceUpdate(d *schema.ResourceData, meta interface{}) } } + // We check for HasChange because we only want to trigger the network architecture upgrade + // if the configuration has explicitly changed. We also check the current value to ensure + // it's true, as this is an irreversible opt-in. + if d.HasChange("enforce_new_sql_network_architecture") && d.Get("enforce_new_sql_network_architecture").(bool) { + instance = &sqladmin.DatabaseInstance{SqlNetworkArchitecture: "NEW_NETWORK_ARCHITECTURE"} + err = transport_tpg.Retry(transport_tpg.RetryOptions{ + RetryFunc: func() (rerr error) { + op, rerr = NewClient(config, userAgent).Instances.Patch(project, d.Get("name").(string), instance).Do() + return rerr + }, + Timeout: d.Timeout(schema.TimeoutUpdate), + ErrorRetryPredicates: []transport_tpg.RetryErrorPredicateFunc{transport_tpg.IsSqlOperationInProgressError}, + }) + if err != nil { + return fmt.Errorf("Error, failed to patch instance settings for %s: %s", d.Get("name").(string), err) + } + err = SqlAdminOperationWaitTime(config, op, project, "Patch Instance", userAgent, d.Timeout(schema.TimeoutUpdate)) + if err != nil { + return err + } + err = resourceSqlDatabaseInstanceRead(d, meta) + if err != nil { + return err + } + } + // Check if the database version is being updated, because patching database version is an atomic operation and can not be // performed with other fields, we first patch database version before updating the rest of the fields. if d.HasChange("database_version") { diff --git a/mmv1/third_party/terraform/services/sql/resource_sql_database_instance_meta.yaml.tmpl b/mmv1/third_party/terraform/services/sql/resource_sql_database_instance_meta.yaml.tmpl index e60e3da2b6bc..5f59440fed5f 100644 --- a/mmv1/third_party/terraform/services/sql/resource_sql_database_instance_meta.yaml.tmpl +++ b/mmv1/third_party/terraform/services/sql/resource_sql_database_instance_meta.yaml.tmpl @@ -74,6 +74,8 @@ fields: - api_field: 'replicaConfiguration.mysqlReplicaConfiguration.verifyServerCertificate' field: 'replica_configuration.verify_server_certificate' - api_field: 'replicaNames' + - api_field: 'sqlNetworkArchitecture' + field: 'enforce_new_sql_network_architecture' - api_field: 'replicationCluster.drReplica' - api_field: 'replicationCluster.failoverDrReplicaName' - api_field: 'replicationCluster.psaWriteEndpoint' diff --git a/mmv1/third_party/terraform/services/sql/resource_sql_database_instance_test.go.tmpl b/mmv1/third_party/terraform/services/sql/resource_sql_database_instance_test.go.tmpl index 856e18ecc5a1..d83398ca9e0c 100644 --- a/mmv1/third_party/terraform/services/sql/resource_sql_database_instance_test.go.tmpl +++ b/mmv1/third_party/terraform/services/sql/resource_sql_database_instance_test.go.tmpl @@ -262,6 +262,40 @@ func TestAccSqlDatabaseInstance_basicSecondGen(t *testing.T) { }) } +func TestAccSqlDatabaseInstance_enforceNewSqlNetworkArchitecture(t *testing.T) { + t.Parallel() + + instanceName := "tf-test-" + acctest.RandString(t, 10) + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccSqlDatabaseInstanceDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccSqlDatabaseInstance_enforceNewSqlNetworkArchitecture(instanceName, true), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_sql_database_instance.instance", "enforce_new_sql_network_architecture", "true"), + ), + }, + { + ResourceName: "google_sql_database_instance.instance", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + { + Config: testAccSqlDatabaseInstance_enforceNewSqlNetworkArchitecture(instanceName, false), + Check: resource.ComposeTestCheckFunc( + // Verify that even though we set it to false in HCL, the state stays true + // and no diff was generated (otherwise this step would fail or show a plan). + resource.TestCheckResourceAttr("google_sql_database_instance.instance", "enforce_new_sql_network_architecture", "true"), + ), + }, + }, + }) +} + func TestAccSqlDatabaseInstance_basicMSSQL(t *testing.T) { t.Parallel() @@ -10203,3 +10237,20 @@ func testGoogleSqlDatabaseInstanceCheckNodeCount(t *testing.T, instance string, return nil } } + +func testAccSqlDatabaseInstance_enforceNewSqlNetworkArchitecture(instanceName string, enforce bool) string { + return fmt.Sprintf(` +resource "google_sql_database_instance" "instance" { + name = "%s" + region = "us-central1" + database_version = "POSTGRES_14" + enforce_new_sql_network_architecture = %t + settings { + tier = "db-f1-micro" + } + + deletion_protection = false +} +`, instanceName, enforce) +} + diff --git a/mmv1/third_party/terraform/website/docs/r/sql_database_instance.html.markdown b/mmv1/third_party/terraform/website/docs/r/sql_database_instance.html.markdown index 0743990ec828..92e771244d29 100644 --- a/mmv1/third_party/terraform/website/docs/r/sql_database_instance.html.markdown +++ b/mmv1/third_party/terraform/website/docs/r/sql_database_instance.html.markdown @@ -399,6 +399,11 @@ includes an up-to-date reference of supported versions. ~> **NOTE:** This flag only protects instances from deletion within Terraform. To protect your instances from accidental deletion across all surfaces (API, gcloud, Cloud Console and Terraform), use the API flag `settings.deletion_protection_enabled`. +* `enforce_new_sql_network_architecture` - (Optional) Whether to enforce the new SQL network architecture. + By default, new Cloud SQL instances created in projects created after August 2021 use the new network architecture. + This follows the gcloud pattern where the flag is an irreversible opt-in. + See [official documentation](https://docs.cloud.google.com/sql/docs/mysql/upgrade-cloud-sql-instance-new-network-architecture#new-arch) for more details. + * `final_backup_description` - (Optional) The description of final backup. Only set this field when `final_backup_config.enabled` is true. * `restore_backup_context` - (optional) The context needed to restore the database to a backup run. This field will From 0d3f87ec482c1ed2bcaca17f5cfdb43bc67b4369 Mon Sep 17 00:00:00 2001 From: patrickmoy <53500820+patrickmoy@users.noreply.github.com> Date: Wed, 8 Jul 2026 09:28:38 -0700 Subject: [PATCH 60/78] Add inspect_config.min_likelihood_per_info_type field to data_loss_prevention_inspect_template (#18221) --- mmv1/products/dlp/InspectTemplate.yaml | 36 ++++++ ...a_loss_prevention_inspect_template_test.go | 113 ++++++++++++++++++ 2 files changed, 149 insertions(+) diff --git a/mmv1/products/dlp/InspectTemplate.yaml b/mmv1/products/dlp/InspectTemplate.yaml index 8830c701352c..668a76b479b6 100644 --- a/mmv1/products/dlp/InspectTemplate.yaml +++ b/mmv1/products/dlp/InspectTemplate.yaml @@ -139,6 +139,42 @@ properties: - POSSIBLE - LIKELY - VERY_LIKELY + - name: minLikelihoodPerInfoType + type: Array + description: | + Minimum likelihood per infotype. For each infotype, a user can specify a minimum likelihood. + The system only returns a finding if its likelihood is above this threshold. If this field + is not set, the system uses the InspectConfig min_likelihood. + item_type: + type: NestedObject + properties: + - name: infoType + type: NestedObject + description: | + Type of information the likeliness threshold applies to. Only one likelihood per info_type should be provided. + If InfoTypeLikelihood does not have an info_type, the configuration fails. + properties: + - name: name + type: String + description: | + Name of the information type. Either a name of your choosing when creating a CustomInfoType, or one of the names listed + at https://cloud.google.com/dlp/docs/infotypes-reference when specifying a built-in type. + required: true + - name: version + type: String + description: | + Version name for this InfoType. + - name: minLikelihood + type: Enum + description: | + Only returns findings equal or above this threshold. See https://cloud.google.com/dlp/docs/likelihood for more info. + required: true + enum_values: + - VERY_UNLIKELY + - UNLIKELY + - POSSIBLE + - LIKELY + - VERY_LIKELY - name: limits type: NestedObject description: Configuration to control the number of findings returned. diff --git a/mmv1/third_party/terraform/services/datalossprevention/resource_data_loss_prevention_inspect_template_test.go b/mmv1/third_party/terraform/services/datalossprevention/resource_data_loss_prevention_inspect_template_test.go index f551007bccca..c86d591671a0 100644 --- a/mmv1/third_party/terraform/services/datalossprevention/resource_data_loss_prevention_inspect_template_test.go +++ b/mmv1/third_party/terraform/services/datalossprevention/resource_data_loss_prevention_inspect_template_test.go @@ -1334,3 +1334,116 @@ resource "google_data_loss_prevention_inspect_template" "basic" { } `, context) } + +func TestAccDataLossPreventionInspectTemplate_dlpInspectTemplate_minLikelihoodPerInfoType(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "project": envvar.GetTestProjectFromEnv(), + } + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccCheckDataLossPreventionInspectTemplateDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccDataLossPreventionInspectTemplate_dlpInspectTemplate_withMinLikelihoodPerInfoType(context), + }, + { + ResourceName: "google_data_loss_prevention_inspect_template.basic", + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccDataLossPreventionInspectTemplate_dlpInspectTemplate_withMinLikelihoodPerInfoTypeUpdate(context), + }, + { + ResourceName: "google_data_loss_prevention_inspect_template.basic", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccDataLossPreventionInspectTemplate_dlpInspectTemplate_withMinLikelihoodPerInfoType(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_data_loss_prevention_inspect_template" "basic" { + parent = "projects/%{project}" + description = "Description" + display_name = "Display" + + inspect_config { + info_types { + name = "EMAIL_ADDRESS" + } + info_types { + name = "PERSON_NAME" + version = "latest" + } + info_types { + name = "LAST_NAME" + } + info_types { + name = "DOMAIN_NAME" + } + info_types { + name = "PHONE_NUMBER" + } + info_types { + name = "FIRST_NAME" + } + min_likelihood_per_info_type { + info_type { + name = "PERSON_NAME" + version = "latest" + } + min_likelihood = "UNLIKELY" + } + } +} +`, context) +} + +func testAccDataLossPreventionInspectTemplate_dlpInspectTemplate_withMinLikelihoodPerInfoTypeUpdate(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_data_loss_prevention_inspect_template" "basic" { + parent = "projects/%{project}" + description = "Updated" + display_name = "Different" + + inspect_config { + info_types { + name = "PERSON_NAME" + version = "latest" + } + info_types { + name = "LAST_NAME" + } + info_types { + name = "DOMAIN_NAME" + } + info_types { + name = "PHONE_NUMBER" + } + info_types { + name = "FIRST_NAME" + } + min_likelihood_per_info_type { + info_type { + name = "PERSON_NAME" + version = "latest" + } + min_likelihood = "POSSIBLE" + } + min_likelihood_per_info_type { + info_type { + name = "PHONE_NUMBER" + } + min_likelihood = "VERY_LIKELY" + } + } +} +`, context) +} From 9f80edf9f821da0d43546aa25c0decf9aed3dcf8 Mon Sep 17 00:00:00 2001 From: Vaibhav Rau Date: Wed, 8 Jul 2026 11:31:22 -0500 Subject: [PATCH 61/78] New data source: google_data_catalog_taxonomy (#18147) --- ...ata_source_google_data_catalog_taxonomy.go | 127 ++++++++++++++++++ ...ource_google_data_catalog_taxonomy_test.go | 51 +++++++ .../d/data_catalog_taxonomy.html.markdown | 49 +++++++ 3 files changed, 227 insertions(+) create mode 100644 mmv1/third_party/terraform/services/datacatalog/data_source_google_data_catalog_taxonomy.go create mode 100644 mmv1/third_party/terraform/services/datacatalog/data_source_google_data_catalog_taxonomy_test.go create mode 100644 mmv1/third_party/terraform/website/docs/d/data_catalog_taxonomy.html.markdown diff --git a/mmv1/third_party/terraform/services/datacatalog/data_source_google_data_catalog_taxonomy.go b/mmv1/third_party/terraform/services/datacatalog/data_source_google_data_catalog_taxonomy.go new file mode 100644 index 000000000000..17dc2edb308c --- /dev/null +++ b/mmv1/third_party/terraform/services/datacatalog/data_source_google_data_catalog_taxonomy.go @@ -0,0 +1,127 @@ +package datacatalog + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-provider-google/google/registry" + "github.com/hashicorp/terraform-provider-google/google/tpgresource" + transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" +) + +func DataSourceGoogleDataCatalogTaxonomy() *schema.Resource { + return &schema.Resource{ + Read: dataSourceGoogleDataCatalogTaxonomyRead, + + Schema: map[string]*schema.Schema{ + "display_name": { + Type: schema.TypeString, + Required: true, + }, + "region": { + Type: schema.TypeString, + Required: true, + }, + "project": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "name": { + Type: schema.TypeString, + Computed: true, + }, + "description": { + Type: schema.TypeString, + Computed: true, + }, + "activated_policy_types": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + } +} + +func dataSourceGoogleDataCatalogTaxonomyRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*transport_tpg.Config) + userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) + if err != nil { + return err + } + + project, err := tpgresource.GetProject(d, config) + if err != nil { + return err + } + + region := d.Get("region").(string) + displayName := d.Get("display_name").(string) + url := fmt.Sprintf("%sprojects/%s/locations/%s/taxonomies", transport_tpg.BaseUrl(Product, config), project, region) + + res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "GET", + RawURL: url, + UserAgent: userAgent, + }) + if err != nil { + return err + } + + taxonomiesRaw, ok := res["taxonomies"] + if !ok { + return fmt.Errorf("No taxonomy found with display_name %q in project %s, region %s", displayName, project, region) + } + + taxonomies, ok := taxonomiesRaw.([]interface{}) + if !ok { + return fmt.Errorf("unexpected taxonomies response format") + } + + for _, taxonomyRaw := range taxonomies { + taxonomy, ok := taxonomyRaw.(map[string]interface{}) + if !ok { + continue + } + + if taxonomy["displayName"] == displayName { + d.SetId(taxonomy["name"].(string)) + + if err := d.Set("name", taxonomy["name"]); err != nil { + return fmt.Errorf("Error setting name: %s", err) + } + if err := d.Set("display_name", taxonomy["displayName"]); err != nil { + return fmt.Errorf("Error setting display_name: %s", err) + } + if err := d.Set("description", taxonomy["description"]); err != nil { + return fmt.Errorf("Error setting description: %s", err) + } + if err := d.Set("activated_policy_types", taxonomy["activatedPolicyTypes"]); err != nil { + return fmt.Errorf("Error setting activated_policy_types: %s", err) + } + if err := d.Set("project", project); err != nil { + return fmt.Errorf("Error setting project: %s", err) + } + if err := d.Set("region", region); err != nil { + return fmt.Errorf("Error setting region: %s", err) + } + + return nil + } + } + + return fmt.Errorf("No taxonomy found with display_name %q in project %s, region %s", displayName, project, region) +} + +func init() { + registry.Schema{ + Name: "google_data_catalog_taxonomy", + ProductName: "datacatalog", + Type: registry.SchemaTypeDataSource, + Schema: DataSourceGoogleDataCatalogTaxonomy(), + }.Register() +} diff --git a/mmv1/third_party/terraform/services/datacatalog/data_source_google_data_catalog_taxonomy_test.go b/mmv1/third_party/terraform/services/datacatalog/data_source_google_data_catalog_taxonomy_test.go new file mode 100644 index 000000000000..906ec4423c0d --- /dev/null +++ b/mmv1/third_party/terraform/services/datacatalog/data_source_google_data_catalog_taxonomy_test.go @@ -0,0 +1,51 @@ +package datacatalog_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-provider-google/google/acctest" + _ "github.com/hashicorp/terraform-provider-google/google/services/datacatalog" +) + +func TestAccDataSourceGoogleDataCatalogTaxonomy_basic(t *testing.T) { + t.Parallel() + + randomSuffix := acctest.RandString(t, 10) + + context := map[string]interface{}{ + "random_suffix": randomSuffix, + } + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + Steps: []resource.TestStep{ + { + Config: testAccDataSourceGoogleDataCatalogTaxonomy_basic(context), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair("data.google_data_catalog_taxonomy.test", "name", "google_data_catalog_taxonomy.test", "name"), + resource.TestCheckResourceAttrPair("data.google_data_catalog_taxonomy.test", "description", "google_data_catalog_taxonomy.test", "description"), + resource.TestCheckResourceAttrPair("data.google_data_catalog_taxonomy.test", "activated_policy_types.#", "google_data_catalog_taxonomy.test", "activated_policy_types.#"), + resource.TestCheckResourceAttrPair("data.google_data_catalog_taxonomy.test", "project", "google_data_catalog_taxonomy.test", "project"), + ), + }, + }, + }) +} + +func testAccDataSourceGoogleDataCatalogTaxonomy_basic(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_data_catalog_taxonomy" "test" { + display_name = "tf_test_%{random_suffix}" + description = "A test taxonomy" + activated_policy_types = ["FINE_GRAINED_ACCESS_CONTROL"] + region = "us-central1" +} + +data "google_data_catalog_taxonomy" "test" { + display_name = google_data_catalog_taxonomy.test.display_name + region = google_data_catalog_taxonomy.test.region +} +`, context) +} diff --git a/mmv1/third_party/terraform/website/docs/d/data_catalog_taxonomy.html.markdown b/mmv1/third_party/terraform/website/docs/d/data_catalog_taxonomy.html.markdown new file mode 100644 index 000000000000..f8c5e14f3044 --- /dev/null +++ b/mmv1/third_party/terraform/website/docs/d/data_catalog_taxonomy.html.markdown @@ -0,0 +1,49 @@ +--- +subcategory: "Data Catalog" +description: |- + Get information about a Google Data Catalog Taxonomy. +--- + +# google_data_catalog_taxonomy + +Get information about a Google Data Catalog Taxonomy. For more information see +[the official documentation](https://cloud.google.com/data-catalog/docs) +and +[API](https://cloud.google.com/data-catalog/docs/reference/rest/v1/projects.locations.taxonomies). + +## Example Usage + +```hcl +data "google_data_catalog_taxonomy" "example" { + display_name = "my_taxonomy" + region = "us-central1" +} + +resource "google_data_catalog_policy_tag" "example" { + taxonomy = data.google_data_catalog_taxonomy.example.id + display_name = "example_policy_tag" + description = "A policy tag" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `display_name` - (Required) The user-defined name of the taxonomy to look up. + +* `region` - (Required) The region the taxonomy is located in. + +* `project` - (Optional) The project in which the taxonomy exists. If not provided, the provider project is used. + +## Attributes Reference + +In addition to the arguments listed above, the following computed attributes are exported: + +* `id` - The full resource name of the taxonomy in the format `projects/{project}/locations/{region}/taxonomies/{taxonomy_id}`. + +* `name` - The full resource name of the taxonomy. + +* `description` - The description of the taxonomy. + +* `activated_policy_types` - A list of policy types activated for this taxonomy. \ No newline at end of file From 68ee821640edf879ff9ea573a65e471b4e2d1045 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Katarzyna=20Ka=C5=84ska?= Date: Wed, 8 Jul 2026 22:49:03 +0200 Subject: [PATCH 62/78] Add google_vector_search_index resource (#18004) --- mmv1/products/vectorsearch/Index.yaml | 188 ++++++++++++++++++ .../vectorsearch_index_basic.tf.tmpl | 57 ++++++ .../vectorsearch_index_dedicated.tf.tmpl | 67 +++++++ .../resource_vector_search_index_test.go | 172 ++++++++++++++++ 4 files changed, 484 insertions(+) create mode 100644 mmv1/products/vectorsearch/Index.yaml create mode 100644 mmv1/templates/terraform/samples/services/vectorsearch/vectorsearch_index_basic.tf.tmpl create mode 100644 mmv1/templates/terraform/samples/services/vectorsearch/vectorsearch_index_dedicated.tf.tmpl create mode 100644 mmv1/third_party/terraform/services/vectorsearch/resource_vector_search_index_test.go diff --git a/mmv1/products/vectorsearch/Index.yaml b/mmv1/products/vectorsearch/Index.yaml new file mode 100644 index 000000000000..b4b1ae4af2f6 --- /dev/null +++ b/mmv1/products/vectorsearch/Index.yaml @@ -0,0 +1,188 @@ +# 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: Index +description: |- + An Index defines an approximate nearest-neighbor search structure over a + field of a Vector Search Collection. +base_url: projects/{{project}}/locations/{{location}}/collections/{{collection_id}}/indexes +self_link: projects/{{project}}/locations/{{location}}/collections/{{collection_id}}/indexes/{{index_id}} +create_url: projects/{{project}}/locations/{{location}}/collections/{{collection_id}}/indexes?indexId={{index_id}} +update_mask: true +update_verb: PATCH +import_format: + - projects/{{project}}/locations/{{location}}/collections/{{collection_id}}/indexes/{{index_id}} +timeouts: + insert_minutes: 90 + update_minutes: 60 + delete_minutes: 60 +async: + operation: + timeouts: + insert_minutes: 90 + update_minutes: 60 + delete_minutes: 60 + base_url: '{{op_id}}' + actions: + - create + - update + - delete + result: + resource_inside_response: true +autogen_status: SW5kZXg= +autogen_async: true +samples: + - name: vectorsearch_index_basic + primary_resource_id: example-index + steps: + - name: vectorsearch_index_basic + resource_id_vars: + index_id: example-index + collection_id: example-collection + - name: vectorsearch_index_dedicated + primary_resource_id: example-dedicated-index + steps: + - name: vectorsearch_index_dedicated + resource_id_vars: + index_id: example-dedicated-index + collection_id: example-dedicated-collection +parameters: + - name: location + type: String + required: true + 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 + - name: collectionId + type: String + required: true + description: |- + The ID of the parent Collection. + immutable: true + url_param_only: true + - name: indexId + type: String + required: true + description: |- + ID of the Index to create. + The id must be 1-63 characters long, and comply with + [RFC1035](https://www.ietf.org/rfc/rfc1035.txt). + Specifically, it must be 1-63 characters long and match the regular + expression `[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?`. + immutable: true + url_param_only: true +properties: + - name: name + type: String + description: Identifier. name of resource + output: true + - name: createTime + type: String + description: '[Output only] Create time stamp' + output: true + - name: updateTime + type: String + description: '[Output only] Update time stamp' + output: true + - name: displayName + type: String + description: User-specified display name of the index + - name: description + type: String + description: User-specified description of the index + - name: labels + type: KeyValueLabels + description: Labels as key value pairs. + - name: distanceMetric + type: Enum + description: |- + Distance metric used for indexing. If not specified, will default to + `DOT_PRODUCT`. + immutable: true + default_from_api: true + enum_values: + - 'DOT_PRODUCT' + - 'COSINE_DISTANCE' + - name: indexField + type: String + required: true + immutable: true + description: The collection schema field to index. + - name: filterFields + type: Array + immutable: true + description: The fields to push into the index to enable fast ANN inline filtering. + item_type: + type: String + - name: storeFields + type: Array + immutable: true + description: The fields to push into the index to enable inline data retrieval. + item_type: + type: String + - name: dedicatedInfrastructure + type: NestedObject + default_from_api: true + description: |- + Dedicated infrastructure for the index. This field belongs to the + `infra_type` oneof; if omitted, the server populates it with the + default `PERFORMANCE_OPTIMIZED` mode and an autoscaling spec of + `min_replica_count=2`, `max_replica_count=2`. + properties: + - name: mode + type: Enum + immutable: true + default_from_api: true + description: |- + Mode of the dedicated infrastructure. Defaults to `PERFORMANCE_OPTIMIZED`. + enum_values: + - 'MODE_UNSPECIFIED' + - 'STORAGE_OPTIMIZED' + - 'PERFORMANCE_OPTIMIZED' + - name: autoscalingSpec + type: NestedObject + default_from_api: true + description: Autoscaling specification. + properties: + - name: minReplicaCount + type: Integer + default_from_api: true + description: |- + The minimum number of replicas. If not set or set to `0`, defaults + to `2`. Must be >= `1` and <= `1000`. + - name: maxReplicaCount + type: Integer + default_from_api: true + description: |- + The maximum number of replicas. Must be >= `min_replica_count` + and <= `1000`. If not set or set to `0`, defaults to the greater + of `min_replica_count` and `2` (or `5` for the v1beta version). + - name: denseScann + type: NestedObject + immutable: true + default_from_api: true + description: |- + Dense ScaNN index configuration. This field belongs to the + `index_type` oneof; if omitted, the server populates it with default + ScaNN settings. + properties: + - name: featureNormType + type: Enum + immutable: true + default_from_api: true + description: Feature norm type for the ScaNN index. + enum_values: + - 'FEATURE_NORM_TYPE_UNSPECIFIED' + - 'NONE' + - 'UNIT_L2_NORM' diff --git a/mmv1/templates/terraform/samples/services/vectorsearch/vectorsearch_index_basic.tf.tmpl b/mmv1/templates/terraform/samples/services/vectorsearch/vectorsearch_index_basic.tf.tmpl new file mode 100644 index 000000000000..59e86c6d6e24 --- /dev/null +++ b/mmv1/templates/terraform/samples/services/vectorsearch/vectorsearch_index_basic.tf.tmpl @@ -0,0 +1,57 @@ +# NOTE: For most workloads we recommend creating the Collection and the Index +# in *separate* Terraform configurations (i.e. create and apply the Collection +# first, ingest data via importDataObjects, and only then create the Index in a +# second configuration). Once an Index exists on a Collection you can no longer +# run importDataObjects for bulk ingestion of data objects on that Collection -- +# you are limited to creating data objects one at a time or in small online +# batches. Defining both resources in the same Terraform file (as shown below) +# is convenient for a quick start, but locks you into the online / batched +# create path for any subsequent data ingestion. +resource "google_vector_search_collection" "parent" { + location = "us-central1" + collection_id = "{{index $.ResourceIdVars "collection_id"}}" + + display_name = "My Awesome Collection" + description = "This collection stores important data." + + data_schema = < Date: Thu, 9 Jul 2026 02:52:38 +0530 Subject: [PATCH 63/78] Terraform Support for Chronicle DataExport (#17973) --- mmv1/products/chronicle/DataExport.yaml | 161 ++++++++++++++++++ .../chronicle_data_export_basic.tf.tmpl | 18 ++ .../chronicle_data_export_full.tf.tmpl | 26 +++ .../terraform/schema_property.go.tmpl | 3 + 4 files changed, 208 insertions(+) create mode 100644 mmv1/products/chronicle/DataExport.yaml create mode 100644 mmv1/templates/terraform/samples/services/chronicle/chronicle_data_export_basic.tf.tmpl create mode 100644 mmv1/templates/terraform/samples/services/chronicle/chronicle_data_export_full.tf.tmpl diff --git a/mmv1/products/chronicle/DataExport.yaml b/mmv1/products/chronicle/DataExport.yaml new file mode 100644 index 000000000000..92fd822a8af4 --- /dev/null +++ b/mmv1/products/chronicle/DataExport.yaml @@ -0,0 +1,161 @@ +# 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: DataExport +description: | + DataExport resource represents a request to export data from Chronicle to a GCS bucket. +references: + guides: + 'Data Export Overview': 'https://cloud.google.com/chronicle/docs/secops/data-export-overview' + api: 'https://cloud.google.com/chronicle/docs/reference/rest/v1beta/projects.locations.instances.dataExports' +base_url: projects/{{project}}/locations/{{location}}/instances/{{instance}}/dataExports +self_link: projects/{{project}}/locations/{{location}}/instances/{{instance}}/dataExports/{{data_export_id}} +create_url: projects/{{project}}/locations/{{location}}/instances/{{instance}}/dataExports +id_format: projects/{{project}}/locations/{{location}}/instances/{{instance}}/dataExports/{{data_export_id}} +import_format: + - projects/{{project}}/locations/{{location}}/instances/{{instance}}/dataExports/{{data_export_id}} +immutable: true +exclude_delete: true +autogen_status: RGF0YUV4cG9ydA== + +samples: + - name: 'chronicle_data_export_basic' + primary_resource_id: 'example' + bootstrap_iam: + - member: "serviceAccount:service-{project_number}@gcp-sa-chronicle.iam.gserviceaccount.com" + role: "roles/storage.admin" + steps: + - name: 'chronicle_data_export_basic' + vars: + location: 'us' + start_time: '2025-01-01T00:00:00Z' + end_time: '2025-01-01T12:00:00Z' + test_env_vars: + chronicle_id: 'CHRONICLE_ID' + project_name: 'PROJECT_NAME' + - name: 'chronicle_data_export_full' + primary_resource_id: 'example' + bootstrap_iam: + - member: "serviceAccount:service-{project_number}@gcp-sa-chronicle.iam.gserviceaccount.com" + role: "roles/storage.admin" + steps: + - name: 'chronicle_data_export_full' + vars: + location: 'us' + start_time: '2025-01-01T00:00:00Z' + end_time: '2025-01-01T12:00:00Z' + test_env_vars: + chronicle_id: 'CHRONICLE_ID' + project_name: 'PROJECT_NAME' + +parameters: + - name: location + type: String + description: The location of the resource. + url_param_only: true + required: true + immutable: true + - name: instance + type: String + description: The unique identifier for the Chronicle instance. + url_param_only: true + required: true + immutable: true +properties: + - name: name + type: String + description: The resource name of the data export. + output: true + - name: dataExportId + type: String + description: The unique identifier for the data export. + output: true + custom_flatten: 'templates/terraform/custom_flatten/id_from_name.tmpl' + - name: startTime + type: String + required: true + description: Start, inclusive time from the range. + - name: endTime + type: String + required: true + description: Last, exclusive time from the range. + - name: gcsBucket + type: String + required: true + description: Link to the destination Cloud Storage bucket. + - name: includeLogTypes + type: Array + default_from_api: true + item_type: + type: String + diff_suppress_func: 'tpgresource.ProjectNumberDiffSuppress' + description: The specific log types to include in the Data Export request. + - name: estimatedVolume + type: Integer + description: The estimated export volume in bytes. + output: true + - name: exportedVolume + type: Integer + description: Actual volume of data exported. + output: true + - name: createTime + type: String + description: Timestamp indicating when the DataExport resource was created. + output: true + - name: updateTime + type: String + description: Timestamp indicating the last time the DataExport resource was updated. + output: true + - name: namespaces + type: Array + item_type: + type: String + description: The namespaces used to filter the export. + - name: ingestionLabels + type: Array + description: The ingestion labels used to filter the export. + item_type: + type: NestedObject + properties: + - name: key + type: String + required: true + description: The key. + - name: value + type: String + required: true + description: The value. + - name: dataExportStatus + type: NestedObject + description: Status of the current export. + output: true + properties: + - name: stage + type: String + description: The stage/status of a given data export request. + output: true + - name: dataRbacFiltered + type: Boolean + description: Indicates whether the data export is filtered by RBAC. + output: true + - name: error + type: String + description: The error message if the stage is FINISHED_FAILURE. + output: true + - name: exportedGlobPatterns + type: Array + item_type: + type: String + description: List of exported glob patterns. + output: true diff --git a/mmv1/templates/terraform/samples/services/chronicle/chronicle_data_export_basic.tf.tmpl b/mmv1/templates/terraform/samples/services/chronicle/chronicle_data_export_basic.tf.tmpl new file mode 100644 index 000000000000..feab2679588e --- /dev/null +++ b/mmv1/templates/terraform/samples/services/chronicle/chronicle_data_export_basic.tf.tmpl @@ -0,0 +1,18 @@ +data "google_project" "project" { + project_id = "{{index $.TestEnvVars "project_name"}}" +} + +resource "google_storage_bucket" "test_bucket" { + name = "chronicle-test-bucket-{{index $.TestEnvVars "project_name"}}-%{random_suffix}" + project = "{{index $.TestEnvVars "project_name"}}" + location = "{{index $.Vars "location"}}" + force_destroy = true +} + +resource "google_chronicle_data_export" "{{$.PrimaryResourceId}}" { + location = "{{index $.Vars "location"}}" + instance = "{{index $.TestEnvVars "chronicle_id"}}" + gcs_bucket = "projects/${data.google_project.project.number}/buckets/${google_storage_bucket.test_bucket.name}" + start_time = "{{index $.Vars "start_time"}}" + end_time = "{{index $.Vars "end_time"}}" +} diff --git a/mmv1/templates/terraform/samples/services/chronicle/chronicle_data_export_full.tf.tmpl b/mmv1/templates/terraform/samples/services/chronicle/chronicle_data_export_full.tf.tmpl new file mode 100644 index 000000000000..26b89e3aedd5 --- /dev/null +++ b/mmv1/templates/terraform/samples/services/chronicle/chronicle_data_export_full.tf.tmpl @@ -0,0 +1,26 @@ +data "google_project" "project" { + project_id = "{{index $.TestEnvVars "project_name"}}" +} + +resource "google_storage_bucket" "test_bucket" { + name = "chronicle-test-bucket-{{index $.TestEnvVars "project_name"}}-%{random_suffix}" + project = "{{index $.TestEnvVars "project_name"}}" + location = "{{index $.Vars "location"}}" + force_destroy = true +} + +resource "google_chronicle_data_export" "{{$.PrimaryResourceId}}" { + location = "{{index $.Vars "location"}}" + instance = "{{index $.TestEnvVars "chronicle_id"}}" + gcs_bucket = "projects/${data.google_project.project.number}/buckets/${google_storage_bucket.test_bucket.name}" + start_time = "{{index $.Vars "start_time"}}" + end_time = "{{index $.Vars "end_time"}}" + include_log_types = ["projects/{{index $.TestEnvVars "project_name"}}/locations/{{index $.Vars "location"}}/instances/{{index $.TestEnvVars "chronicle_id"}}/logTypes/GCP_CLOUDAUDIT"] + + ingestion_labels { + key = "key1" + value = "val1" + } + + namespaces = ["my-namespace"] +} diff --git a/mmv1/templates/terraform/schema_property.go.tmpl b/mmv1/templates/terraform/schema_property.go.tmpl index 34cca0618ed6..ccb99efc1b52 100644 --- a/mmv1/templates/terraform/schema_property.go.tmpl +++ b/mmv1/templates/terraform/schema_property.go.tmpl @@ -103,6 +103,9 @@ Default value: {{ .ItemType.DefaultValue -}} {{ else if eq .ItemType.Type "String" -}} Elem: &schema.Schema{ Type: schema.Type{{ .ItemTypeClass -}}, + {{- if .ItemType.DiffSuppressFunc }} + DiffSuppressFunc: {{ .ItemType.DiffSuppressFunc }}, + {{- end }} {{ template "ItemValidation" . -}} }, {{ else if eq .ItemType.Type "Enum" -}} From 369ab179cf0cee2bb594eb8273127eaa339708dd Mon Sep 17 00:00:00 2001 From: nimam Date: Wed, 8 Jul 2026 15:02:42 -0700 Subject: [PATCH 64/78] Add support for Chronicle Findings Refinement Deployment resource in GA (#18111) --- mmv1/products/chronicle/FindingsRefinementDeployment.yaml | 4 +--- .../chronicle_findings_refinement_deployment_full.tf.tmpl | 2 -- .../chronicle_findings_refinement_deployment_update.tf.tmpl | 2 -- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/mmv1/products/chronicle/FindingsRefinementDeployment.yaml b/mmv1/products/chronicle/FindingsRefinementDeployment.yaml index ad43f76c08ab..d0e94b400043 100644 --- a/mmv1/products/chronicle/FindingsRefinementDeployment.yaml +++ b/mmv1/products/chronicle/FindingsRefinementDeployment.yaml @@ -17,8 +17,7 @@ description: The FindingsRefinementDeployment resource represents the deployment references: guides: 'Google SecOps Guides': 'https://cloud.google.com/chronicle/docs/secops/secops-overview' - api: 'https://docs.cloud.google.com/chronicle/docs/reference/rest/v1beta/FindingsRefinementDeployment' -min_version: 'beta' + api: 'https://docs.cloud.google.com/chronicle/docs/reference/rest/v1/FindingsRefinementDeployment' base_url: projects/{{project}}/locations/{{location}}/instances/{{instance}}/findingsRefinements/{{findings_refinement}}/deployment self_link: projects/{{project}}/locations/{{location}}/instances/{{instance}}/findingsRefinements/{{findings_refinement}}/deployment create_url: projects/{{project}}/locations/{{location}}/instances/{{instance}}/findingsRefinements/{{findings_refinement}}/deployment?updateMask=* @@ -34,7 +33,6 @@ autogen_status: RmluZGluZ3NSZWZpbmVtZW50RGVwbG95bWVudA== samples: - name: 'chronicle_findings_refinement_deployment_update' primary_resource_id: 'example' - min_version: 'beta' steps: - name: 'chronicle_findings_refinement_deployment_full' test_env_vars: diff --git a/mmv1/templates/terraform/samples/services/chronicle/chronicle_findings_refinement_deployment_full.tf.tmpl b/mmv1/templates/terraform/samples/services/chronicle/chronicle_findings_refinement_deployment_full.tf.tmpl index ab0128e8bec3..4c162090bca8 100644 --- a/mmv1/templates/terraform/samples/services/chronicle/chronicle_findings_refinement_deployment_full.tf.tmpl +++ b/mmv1/templates/terraform/samples/services/chronicle/chronicle_findings_refinement_deployment_full.tf.tmpl @@ -1,5 +1,4 @@ resource "google_chronicle_findings_refinement" "my-findings-refinement" { - provider = google-beta location = "us" instance = "{{index $.TestEnvVars "chronicle_id"}}" display_name = "{{index $.ResourceIdVars "display_name"}}" @@ -13,7 +12,6 @@ resource "google_chronicle_findings_refinement" "my-findings-refinement" { } resource "google_chronicle_findings_refinement_deployment" "{{$.PrimaryResourceId}}" { - provider = google-beta location = "us" instance = "{{index $.TestEnvVars "chronicle_id"}}" findings_refinement = element(split("/", resource.google_chronicle_findings_refinement.my-findings-refinement.name), length(split("/", resource.google_chronicle_findings_refinement.my-findings-refinement.name)) - 1) diff --git a/mmv1/templates/terraform/samples/services/chronicle/chronicle_findings_refinement_deployment_update.tf.tmpl b/mmv1/templates/terraform/samples/services/chronicle/chronicle_findings_refinement_deployment_update.tf.tmpl index ab0128e8bec3..4c162090bca8 100644 --- a/mmv1/templates/terraform/samples/services/chronicle/chronicle_findings_refinement_deployment_update.tf.tmpl +++ b/mmv1/templates/terraform/samples/services/chronicle/chronicle_findings_refinement_deployment_update.tf.tmpl @@ -1,5 +1,4 @@ resource "google_chronicle_findings_refinement" "my-findings-refinement" { - provider = google-beta location = "us" instance = "{{index $.TestEnvVars "chronicle_id"}}" display_name = "{{index $.ResourceIdVars "display_name"}}" @@ -13,7 +12,6 @@ resource "google_chronicle_findings_refinement" "my-findings-refinement" { } resource "google_chronicle_findings_refinement_deployment" "{{$.PrimaryResourceId}}" { - provider = google-beta location = "us" instance = "{{index $.TestEnvVars "chronicle_id"}}" findings_refinement = element(split("/", resource.google_chronicle_findings_refinement.my-findings-refinement.name), length(split("/", resource.google_chronicle_findings_refinement.my-findings-refinement.name)) - 1) From be1ab85b54059ab11e08fc781a02d923ecb733c4 Mon Sep 17 00:00:00 2001 From: fuzmish <71511927+fuzmish@users.noreply.github.com> Date: Thu, 9 Jul 2026 07:04:47 +0900 Subject: [PATCH 65/78] add CATALOG_TYPE_FEDERATED support to google_biglake_iceberg_catalog (#18022) --- .../biglakeiceberg/IcebergCatalog.yaml | 159 +++++++++++++++++- .../biglake_iceberg_catalog_update.go.tmpl | 36 ++++ ...ake_iceberg_catalog_federated_glue.tf.tmpl | 18 ++ ...ke_iceberg_catalog_federated_unity.tf.tmpl | 18 ++ 4 files changed, 230 insertions(+), 1 deletion(-) create mode 100644 mmv1/templates/terraform/samples/services/biglakeiceberg/biglake_iceberg_catalog_federated_glue.tf.tmpl create mode 100644 mmv1/templates/terraform/samples/services/biglakeiceberg/biglake_iceberg_catalog_federated_unity.tf.tmpl diff --git a/mmv1/products/biglakeiceberg/IcebergCatalog.yaml b/mmv1/products/biglakeiceberg/IcebergCatalog.yaml index 24a08d3688e8..1749e708461a 100644 --- a/mmv1/products/biglakeiceberg/IcebergCatalog.yaml +++ b/mmv1/products/biglakeiceberg/IcebergCatalog.yaml @@ -69,6 +69,24 @@ samples: test_env_vars: GOOGLE_BILLING_PROJECT: PROJECT USER_PROJECT_OVERRIDE: "true" + - name: biglake_iceberg_catalog_federated_unity + primary_resource_id: my_iceberg_catalog + steps: + - name: biglake_iceberg_catalog_federated_unity + resource_id_vars: + name: my_iceberg_catalog + test_env_vars: + GOOGLE_BILLING_PROJECT: PROJECT + USER_PROJECT_OVERRIDE: "true" + - name: biglake_iceberg_catalog_federated_glue + primary_resource_id: my_iceberg_catalog + steps: + - name: biglake_iceberg_catalog_federated_glue + resource_id_vars: + name: my_iceberg_catalog + test_env_vars: + GOOGLE_BILLING_PROJECT: PROJECT + USER_PROJECT_OVERRIDE: "true" parameters: - name: name type: String @@ -105,16 +123,26 @@ properties: type: String description: Output only. The service account used for credential vending. It might be empty if credential vending was never enabled for the catalog. output: true + - name: biglake_service_account_id + api_name: biglake-service-account-id + type: String + description: Output only. The unique ID of the service account used for credential vending. Used for federation scenarios. + output: true - name: catalog_type api_name: catalog-type type: Enum - description: The catalog type of the IcebergCatalog. + description: | + The catalog type of the IcebergCatalog. + * `CATALOG_TYPE_GCS_BUCKET`: Google Cloud Storage bucket catalog type. + * `CATALOG_TYPE_BIGLAKE`: BigLake catalog type. + * `CATALOG_TYPE_FEDERATED`: Federated catalog type, for integrating with external Iceberg REST Catalogs such as Databricks Unity Catalog or AWS Glue. required: true immutable: true output: false enum_values: - CATALOG_TYPE_GCS_BUCKET - CATALOG_TYPE_BIGLAKE + - CATALOG_TYPE_FEDERATED - name: default_location api_name: default-location type: String @@ -141,6 +169,9 @@ properties: type: String description: Output only. The last modification time of the IcebergCatalog. output: true + - name: description + type: String + description: A user-provided description of the catalog. Maximum 1024 UTF-8 characters. - name: replicas type: Array item_type: @@ -178,3 +209,129 @@ properties: A list of GCS locations (e.g., `gs://my-other-bucket/...`) that are permitted for use by resources within this catalog. Each entry can be either a GCS bucket or a path within it. + - name: federated_catalog_options + api_name: federated-catalog-options + type: NestedObject + description: | + Options for a CATALOG_TYPE_FEDERATED catalog. Required when catalog_type + is CATALOG_TYPE_FEDERATED. + properties: + - name: secret_name + api_name: secret-name + type: String + description: | + The secret resource name in Secret Manager, in the format + `projects/{projectId}/locations/{location}/secrets/{secret_id}`. + Used to store credentials for authenticating with the remote catalog. + - name: service_directory_name + api_name: service-directory-name + type: String + description: | + The Service Directory service name for private network connectivity + through Cross-Cloud Interconnect. + - name: refresh_options + api_name: refresh-options + type: NestedObject + description: Configuration for metadata synchronization from the remote catalog. + properties: + - name: refresh_schedule + api_name: refresh-schedule + type: NestedObject + description: Schedule for periodic metadata refresh. + properties: + - name: refresh_interval + api_name: refresh-interval + type: String + description: | + The interval between metadata refreshes, expressed as a duration + string (e.g., `300s`). + The value must be at least 300s or 0s to disable refresh. + - name: refresh_scope + api_name: refresh-scope + type: NestedObject + description: Scope of metadata to synchronize from the remote catalog. + properties: + - name: namespace_filters + api_name: namespace-filters + type: Array + item_type: + type: String + description: | + A list of namespace filters to limit which namespaces are + synchronized from the remote catalog. + - name: refresh_status + api_name: refresh-status + type: NestedObject + description: Output only. The status of the most recent metadata refresh. + output: true + properties: + - name: start_time + api_name: start-time + type: String + description: Output only. The start time of the most recent refresh. + output: true + - name: end_time + api_name: end-time + type: String + description: Output only. The end time of the most recent refresh. + output: true + - name: status + type: NestedObject + description: Output only. The error result of the last failed refresh, if any. + output: true + properties: + - name: code + type: Integer + description: Output only. The status code, which should be an enum value of google.rpc.Code. + output: true + - name: message + type: String + description: Output only. A developer-facing error message in English. + output: true + - name: unity_catalog_info + api_name: unity-catalog-info + type: NestedObject + description: | + Configuration for a Databricks Unity Catalog remote catalog. Exactly + one of unity_catalog_info or glue_catalog_info must be specified. + properties: + - name: instance_name + api_name: instance-name + type: String + description: The Databricks workspace instance name. + required: true + immutable: true + - name: catalog_name + api_name: catalog-name + type: String + description: The name of the catalog within the Unity Catalog instance. + required: true + immutable: true + - name: service_principal_application_id + api_name: service-principal-application-id + type: String + description: The application ID of the Databricks service principal for OIDC authentication. + - name: glue_catalog_info + api_name: glue-catalog-info + type: NestedObject + description: | + Configuration for an AWS Glue remote catalog. Exactly one of + unity_catalog_info or glue_catalog_info must be specified. + properties: + - name: warehouse + api_name: warehouse + type: String + description: The AWS Glue warehouse identifier (account ID or S3 table bucket). + required: true + immutable: true + - name: aws_region + api_name: aws-region + type: String + description: The AWS region where the Glue catalog is located. + required: true + immutable: true + - name: aws_role_arn + api_name: aws-role-arn + type: String + description: The ARN of the AWS IAM role to assume for accessing the Glue catalog. + required: true diff --git a/mmv1/templates/terraform/custom_update/biglake_iceberg_catalog_update.go.tmpl b/mmv1/templates/terraform/custom_update/biglake_iceberg_catalog_update.go.tmpl index 7423b49f88e0..51bd1d6942df 100644 --- a/mmv1/templates/terraform/custom_update/biglake_iceberg_catalog_update.go.tmpl +++ b/mmv1/templates/terraform/custom_update/biglake_iceberg_catalog_update.go.tmpl @@ -32,6 +32,20 @@ if err != nil { obj["restricted-locations-config"] = restrictedLocationsConfigProp } +descriptionProp, err := expandBiglakeIcebergIcebergCatalogDescription(d.Get("description"), d, config) +if err != nil { + return err +} else if v, ok := d.GetOkExists("description"); !tpgresource.IsEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, descriptionProp)) { + obj["description"] = descriptionProp +} + +federatedCatalogOptionsProp, err := expandBiglakeIcebergIcebergCatalogFederatedCatalogOptions(d.Get("federated_catalog_options"), d, config) +if err != nil { + return err +} else if v, ok := d.GetOkExists("federated_catalog_options"); !tpgresource.IsEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, federatedCatalogOptionsProp)) { + obj["federated-catalog-options"] = federatedCatalogOptionsProp +} + url, err := tpgresource.ReplaceVars(d, config, "{{"{{"}}BiglakeIcebergBasePath{{"}}"}}iceberg/v1/restcatalog/extensions/projects/{{"{{"}}project{{"}}"}}/catalogs/{{"{{"}}name{{"}}"}}") if err != nil { return err @@ -56,6 +70,28 @@ if d.HasChange("restricted_locations_config") { updateMask = append(updateMask, "restricted_locations_config") } +if d.HasChange("description") { + updateMask = append(updateMask, "description") +} + +if d.HasChange("federated_catalog_options") { + if d.HasChange("federated_catalog_options.0.secret_name") { + updateMask = append(updateMask, "federated_catalog_options.secret_name") + } + if d.HasChange("federated_catalog_options.0.service_directory_name") { + updateMask = append(updateMask, "federated_catalog_options.service_directory_name") + } + if d.HasChange("federated_catalog_options.0.refresh_options") { + updateMask = append(updateMask, "federated_catalog_options.refresh_options") + } + if d.HasChange("federated_catalog_options.0.glue_catalog_info.0.aws_role_arn") { + updateMask = append(updateMask, "federated_catalog_options.glue_catalog_info.aws_role_arn") + } + if d.HasChange("federated_catalog_options.0.unity_catalog_info.0.service_principal_application_id") { + updateMask = append(updateMask, "federated_catalog_options.unity_catalog_info.service_principal_application_id") + } +} + // updateMask is a URL parameter but not present in the schema, so ReplaceVars // won't set it url, err = transport_tpg.AddQueryParams(url, map[string]string{"updateMask": strings.Join(updateMask, ",")}) diff --git a/mmv1/templates/terraform/samples/services/biglakeiceberg/biglake_iceberg_catalog_federated_glue.tf.tmpl b/mmv1/templates/terraform/samples/services/biglakeiceberg/biglake_iceberg_catalog_federated_glue.tf.tmpl new file mode 100644 index 000000000000..ed8d7d0a6f79 --- /dev/null +++ b/mmv1/templates/terraform/samples/services/biglakeiceberg/biglake_iceberg_catalog_federated_glue.tf.tmpl @@ -0,0 +1,18 @@ +resource "google_biglake_iceberg_catalog" "{{$.PrimaryResourceId}}" { + catalog_type = "CATALOG_TYPE_FEDERATED" + name = "{{index $.ResourceIdVars "name"}}" + primary_location = "us-central1" + + federated_catalog_options { + glue_catalog_info { + aws_region = "us-east-1" + aws_role_arn = "arn:aws:iam::111222333444:role/my-glue-role" + warehouse = "111222333444:s3tablescatalog/example" + } + refresh_options { + refresh_schedule { + refresh_interval = "300s" + } + } + } +} diff --git a/mmv1/templates/terraform/samples/services/biglakeiceberg/biglake_iceberg_catalog_federated_unity.tf.tmpl b/mmv1/templates/terraform/samples/services/biglakeiceberg/biglake_iceberg_catalog_federated_unity.tf.tmpl new file mode 100644 index 000000000000..95dbb4ca812b --- /dev/null +++ b/mmv1/templates/terraform/samples/services/biglakeiceberg/biglake_iceberg_catalog_federated_unity.tf.tmpl @@ -0,0 +1,18 @@ +resource "google_biglake_iceberg_catalog" "{{$.PrimaryResourceId}}" { + catalog_type = "CATALOG_TYPE_FEDERATED" + name = "{{index $.ResourceIdVars "name"}}" + primary_location = "us-central1" + + federated_catalog_options { + unity_catalog_info { + catalog_name = "my_catalog" + instance_name = "1.1.gcp.databricks.com" + service_principal_application_id = "b3204274-6556-4d40-ad18-556f91659745" + } + refresh_options { + refresh_schedule { + refresh_interval = "300s" + } + } + } +} From 33cc7dc9be451313a590017536d385394569786b Mon Sep 17 00:00:00 2001 From: Shuya Ma <87669292+shuyama1@users.noreply.github.com> Date: Wed, 8 Jul 2026 15:35:30 -0700 Subject: [PATCH 66/78] remove Examples-based test guide (#18230) --- docs/content/test/test.md | 58 --------------------------------------- 1 file changed, 58 deletions(-) diff --git a/docs/content/test/test.md b/docs/content/test/test.md index 7c7dd12c69e6..9f95022869a0 100644 --- a/docs/content/test/test.md +++ b/docs/content/test/test.md @@ -258,64 +258,6 @@ An update test ensures that updatable fields can be changed without recreating t {{< /tab >}} {{% /tabs %}} -## Legacy: Examples-based Framework - -> [!WARNING] -> The `examples` block is legacy and has been replaced by `samples`. This footprint is still supported for approximately 2 months to allow for transition. New tests should use the `samples` framework. - -Tests within the legacy `examples` generator are configured directly on the resource YAML. **Note that this legacy framework only supports generating create tests;** multi-step update tests must be configured via the new `samples` framework or handwritten manual testing procedures. - -### Steps - -1. Add an entry to your `RESOURCE_NAME.yaml` file's `examples`. The fields listed here are the most commonly-used. For a comprehensive reference, see [MMv1 resource reference: `examples` ↗]({{}}). - ```yaml - examples: - # name must correspond to a configuration file that you'll create in the next step. - # The name should include the product name, resource name, and a basic description - # of the test. This will be used to generate the test name and the documentation - # header. - - name: "PRODUCT_RESOURCE_basic" - # primary_resource_id will be used for the Terraform resource id in the configuration file. - primary_resource_id: "example" - # vars contains key/value pairs of variables to inject into the configuration file. - # These can be referenced in the configuration file as a key inside `{{$.Vars}}`. - # All resource IDs (even for resources not under test) should be declared - # with variables that contain a `-` or `_`; this will ensure that, in tests, - # the resources are created with a `tf-test` prefix to allow automatic cleanup - # of dangling resources and a random suffix to avoid name collisions. - vars: - network_name: "example-network" - # test_vars_overrides contains key/value pairs of literal overrides for - # variables used in tests. This can be used to call functions to - # generate or determine a variable's value – for example, bootstrapping - # a shared network for your product to avoid test failures due to limits - # on the default network. - test_vars_overrides: - network_name: 'servicenetworking.BootstrapSharedServiceNetworkingConnection(t, "PRODUCT-RESOURCE-network-config")' - # Set min_version: beta if the resource is not beta-only and any beta-only fields are being tested. - min_version: beta - ``` - -2. Create a `.tf.tmpl` file in [`mmv1/templates/terraform/examples/`](https://github.com/GoogleCloudPlatform/magic-modules/tree/main/mmv1/templates/terraform/examples). The name of the file should match the name of the example created in the previous step. For example, `PRODUCT_RESOURCE_basic.tf.tmpl`. -3. In that file, write the Terraform configuration for your test. This should include all of the required dependencies. For example, `google_compute_subnetwork` has a dependency on `google_compute_network`: - ```tf - resource "google_compute_subnetwork" "{{$.PrimaryResourceId}}" { - name = "{{index $.Vars "subnetwork_name"}}" - ip_cidr_range = "10.1.0.0/16" - region = "us-central1" - network = google_compute_network.network.name - } - - resource "google_compute_network" "network" { - name = "{{index $.Vars "network_name"}}" - auto_create_subnetworks = false - } - ``` -4. If the resource or the example is beta-only: - - Add `provider = google-beta` to every resource in the file. - -For instructions on how to migrate existing `examples` over to `samples`, see the [Test Template Migration Guide]({{< ref "/reference/update-test-changes" >}}). - ## Bootstrap API resources {#bootstrapping} Most acceptance tests run in a the default org and default test project, which means that they can conflict for quota, resource namespaces, and control over shared resources. You can work around these limitations with "bootstrapped" resources. From 9f95c454885397c8dbeded9f87f41bb698d0df53 Mon Sep 17 00:00:00 2001 From: Majed Alfaraj <98921162+majedalfaraj@users.noreply.github.com> Date: Wed, 8 Jul 2026 16:05:01 -0700 Subject: [PATCH 67/78] test(magician): refactor test-terraform-vcr to use testing sandbox (#17956) --- .ci/magician/cmd/test_terraform_vcr.go | 32 ++++++++++++--------- .ci/magician/cmd/test_terraform_vcr_test.go | 31 +++++++++++--------- 2 files changed, 35 insertions(+), 28 deletions(-) diff --git a/.ci/magician/cmd/test_terraform_vcr.go b/.ci/magician/cmd/test_terraform_vcr.go index b5d15f7d0aff..202127877f43 100644 --- a/.ci/magician/cmd/test_terraform_vcr.go +++ b/.ci/magician/cmd/test_terraform_vcr.go @@ -168,7 +168,7 @@ The following environment variables are required: return fmt.Errorf("error creating VCR tester: %w", err) } - return execTestTerraformVCR(args[0], args[1], args[2], args[3], args[4], baseBranch, gh, rnr, ctlr, vt) + return execTestTerraformVCR(args[0], args[1], args[2], args[3], args[4], baseBranch, "/workspace", gh, rnr, ctlr, vt) }, } @@ -180,7 +180,7 @@ func listTTVRequiredEnvironmentVariables() string { return result } -func execTestTerraformVCR(prNumber, mmCommitSha, buildID, projectID, buildStep, baseBranch string, gh GithubClient, rnr ExecRunner, ctlr *source.Controller, vt *vcr.Tester) error { +func execTestTerraformVCR(prNumber, mmCommitSha, buildID, projectID, buildStep, baseBranch, workspace string, gh GithubClient, rnr ExecRunner, ctlr *source.Controller, vt *vcr.Tester) error { newBranch := "auto-pr-" + prNumber oldBranch := newBranch + "-old" @@ -251,13 +251,13 @@ func execTestTerraformVCR(prNumber, mmCommitSha, buildID, projectID, buildStep, return fmt.Errorf("error uploading replaying logs: %w", err) } - if hasPanics, err := handlePanics(prNumber, buildID, buildStepUrl, mmCommitSha, replayingResult, vcr.Replaying, gh, rnr); err != nil { + if hasPanics, err := handlePanics(prNumber, buildID, buildStepUrl, mmCommitSha, workspace, replayingResult, vcr.Replaying, gh, rnr); err != nil { return fmt.Errorf("error handling panics: %w", err) } else if hasPanics { return nil } - if hasBuildFailures, err := handleBuildFailures(prNumber, buildID, buildStepUrl, mmCommitSha, replayingResult, vcr.Replaying, gh, rnr); err != nil { + if hasBuildFailures, err := handleBuildFailures(prNumber, buildID, buildStepUrl, mmCommitSha, workspace, replayingResult, vcr.Replaying, gh, rnr); err != nil { return fmt.Errorf("error handling build failures: %w", err) } else if hasBuildFailures { return nil @@ -293,7 +293,7 @@ func execTestTerraformVCR(prNumber, mmCommitSha, buildID, projectID, buildStep, comment = fmt.Sprintf("%s\n\n%s VCR tests complete for %s!", comment, mentionStr, mmCommitSha) } } - if err := appendVCRResultToDiffComment(prNumber, comment, gh, rnr); err != nil { + if err := appendVCRResultToDiffComment(prNumber, comment, workspace, gh, rnr); err != nil { return fmt.Errorf("error appending comment: %w", err) } if len(replayingResult.FailedTests) > 0 { @@ -324,13 +324,13 @@ func execTestTerraformVCR(prNumber, mmCommitSha, buildID, projectID, buildStep, return fmt.Errorf("error uploading recording logs: %w", err) } - if hasPanics, err := handlePanics(prNumber, buildID, buildStepUrl, mmCommitSha, recordingResult, vcr.Recording, gh, rnr); err != nil { + if hasPanics, err := handlePanics(prNumber, buildID, buildStepUrl, mmCommitSha, workspace, recordingResult, vcr.Recording, gh, rnr); err != nil { return fmt.Errorf("error handling panics: %w", err) } else if hasPanics { return nil } - if hasBuildFailures, err := handleBuildFailures(prNumber, buildID, buildStepUrl, mmCommitSha, recordingResult, vcr.Recording, gh, rnr); err != nil { + if hasBuildFailures, err := handleBuildFailures(prNumber, buildID, buildStepUrl, mmCommitSha, workspace, recordingResult, vcr.Recording, gh, rnr); err != nil { return fmt.Errorf("error handling build failures: %w", err) } else if hasBuildFailures { return nil @@ -400,7 +400,7 @@ func execTestTerraformVCR(prNumber, mmCommitSha, buildID, projectID, buildStep, if mentionStr != "" { recordReplayComment = fmt.Sprintf("%s\n\n%s VCR tests complete for %s!", recordReplayComment, mentionStr, mmCommitSha) } - if err := appendVCRResultToDiffComment(prNumber, recordReplayComment, gh, rnr); err != nil { + if err := appendVCRResultToDiffComment(prNumber, recordReplayComment, workspace, gh, rnr); err != nil { return fmt.Errorf("error appending comment: %w", err) } } @@ -553,7 +553,7 @@ func runReplaying(runFullVCR bool, version provider.Version, services map[string return result, testDirs, replayingErr } -func handlePanics(prNumber, buildID, buildStepUrl, mmCommitSha string, result vcr.Result, mode vcr.Mode, gh GithubClient, rnr ExecRunner) (bool, error) { +func handlePanics(prNumber, buildID, buildStepUrl, mmCommitSha, workspace string, result vcr.Result, mode vcr.Mode, gh GithubClient, rnr ExecRunner) (bool, error) { if len(result.Panics) > 0 { comment := "> [!CAUTION]\n" comment += "> **Panic occurred during VCR tests**\n>\n" @@ -575,7 +575,7 @@ func handlePanics(prNumber, buildID, buildStepUrl, mmCommitSha string, result vc } comment = header + comment - if err := appendVCRResultToDiffComment(prNumber, comment, gh, rnr); err != nil { + if err := appendVCRResultToDiffComment(prNumber, comment, workspace, gh, rnr); err != nil { return true, fmt.Errorf("error appending comment: %v", err) } if err := gh.PostBuildStatus(prNumber, "VCR-test", "failure", buildStepUrl, mmCommitSha); err != nil { @@ -586,7 +586,7 @@ func handlePanics(prNumber, buildID, buildStepUrl, mmCommitSha string, result vc return false, nil } -func handleBuildFailures(prNumber, buildID, buildStepUrl, mmCommitSha string, result vcr.Result, mode vcr.Mode, gh GithubClient, rnr ExecRunner) (bool, error) { +func handleBuildFailures(prNumber, buildID, buildStepUrl, mmCommitSha, workspace string, result vcr.Result, mode vcr.Mode, gh GithubClient, rnr ExecRunner) (bool, error) { if len(result.BuildFailures) > 0 { comment := "> [!CAUTION]\n" comment += "> **Build Failure during VCR tests**\n>\n" @@ -611,7 +611,7 @@ func handleBuildFailures(prNumber, buildID, buildStepUrl, mmCommitSha string, re } comment = header + comment - if err := appendVCRResultToDiffComment(prNumber, comment, gh, rnr); err != nil { + if err := appendVCRResultToDiffComment(prNumber, comment, workspace, gh, rnr); err != nil { return true, fmt.Errorf("error appending comment: %v", err) } if err := gh.PostBuildStatus(prNumber, "VCR-test", "failure", buildStepUrl, mmCommitSha); err != nil { @@ -654,11 +654,15 @@ func getMentions(prNumber string, gh GithubClient) string { // appendVCRResultToDiffComment appends content to the existing diff report comment // identified by the ID in /workspace/diff_comment_id.txt. // If the file is missing or the comment cannot be fetched, it falls back to posting a new comment. -func appendVCRResultToDiffComment(prNumber string, content string, gh GithubClient, rnr ExecRunner) error { +func appendVCRResultToDiffComment(prNumber string, content string, workspace string, gh GithubClient, rnr ExecRunner) error { var diffComment *github.PullRequestComment + if workspace == "" { + workspace = "/workspace" + } + // Try to find by ID from file - if idStr, err := rnr.ReadFile("/workspace/diff_comment_id.txt"); err == nil { + if idStr, err := rnr.ReadFile(filepath.Join(workspace, "diff_comment_id.txt")); err == nil { if id, err := strconv.Atoi(strings.TrimSpace(idStr)); err == nil { if comment, err := gh.GetPullRequestComment(id); err == nil { diffComment = &comment diff --git a/.ci/magician/cmd/test_terraform_vcr_test.go b/.ci/magician/cmd/test_terraform_vcr_test.go index d7e6cf902840..3256a71697fd 100644 --- a/.ci/magician/cmd/test_terraform_vcr_test.go +++ b/.ci/magician/cmd/test_terraform_vcr_test.go @@ -663,8 +663,9 @@ func TestHandleBuildFailures(t *testing.T) { BuildFailures: []string{"package1", "package2"}, } - rnr := &mockRunner{} - handled, err := handleBuildFailures("123", "build-456", "http://target", "sha789", result, vcr.Replaying, gh, rnr) + sb := newSandbox(t) + sb.RequireAllowlist() + handled, err := handleBuildFailures("123", "build-456", "http://target", "sha789", sb.Dir, result, vcr.Replaying, gh, sb.Runner) assert.NoError(t, err) assert.True(t, handled) @@ -690,8 +691,9 @@ func TestHandleBuildFailures_Recording(t *testing.T) { BuildFailures: []string{"package1", "package2"}, } - rnr := &mockRunner{} - handled, err := handleBuildFailures("123", "build-456", "http://target", "sha789", result, vcr.Recording, gh, rnr) + sb := newSandbox(t) + sb.RequireAllowlist() + handled, err := handleBuildFailures("123", "build-456", "http://target", "sha789", sb.Dir, result, vcr.Recording, gh, sb.Runner) assert.NoError(t, err) assert.True(t, handled) @@ -709,8 +711,9 @@ func TestHandleBuildFailures_NoFailures(t *testing.T) { } result := vcr.Result{} - rnr := &mockRunner{} - handled, err := handleBuildFailures("123", "build-456", "http://target", "sha789", result, vcr.Replaying, gh, rnr) + sb := newSandbox(t) + sb.RequireAllowlist() + handled, err := handleBuildFailures("123", "build-456", "http://target", "sha789", sb.Dir, result, vcr.Replaying, gh, sb.Runner) assert.NoError(t, err) assert.False(t, handled) @@ -736,8 +739,9 @@ func TestAppendVCRResultToDiffComment_NotExists(t *testing.T) { }, } - rnr := &mockRunner{} - err := appendVCRResultToDiffComment("123", "VCR Results", gh, rnr) + sb := newSandbox(t) + sb.RequireAllowlist() + err := appendVCRResultToDiffComment("123", "VCR Results", sb.Dir, gh, sb.Runner) assert.NoError(t, err) assert.Len(t, gh.calledMethods["PostComment"], 1) @@ -761,13 +765,12 @@ func TestAppendVCRResultToDiffComment_UseFileID(t *testing.T) { }, }, } - rnr := &mockRunner{ - fileContents: map[string]string{ - "/workspace/diff_comment_id.txt": "456", - }, - } + sb := newSandbox(t) + sb.RequireAllowlist() + + sb.Runner.WriteFile("diff_comment_id.txt", "456") - err := appendVCRResultToDiffComment("123", "VCR Results", gh, rnr) + err := appendVCRResultToDiffComment("123", "VCR Results", sb.Dir, gh, sb.Runner) assert.NoError(t, err) assert.Len(t, gh.calledMethods["UpdateComment"], 1) From 84eeeb9772f1ad2b330aa06da89168392e474092 Mon Sep 17 00:00:00 2001 From: Farrukh Tashpulatov <66369664+farrukh-t@users.noreply.github.com> Date: Thu, 9 Jul 2026 19:04:55 +0300 Subject: [PATCH 68/78] Bugfix/add missing update mask for additional_options in datastream mongodb connection profile (#18130) --- .../terraform/pre_update/datastream_connection_profile.go.tmpl | 3 +++ .../datastream/resource_datastream_connection_profile_test.go | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/mmv1/templates/terraform/pre_update/datastream_connection_profile.go.tmpl b/mmv1/templates/terraform/pre_update/datastream_connection_profile.go.tmpl index e9d50eb50034..62f0723ae18e 100644 --- a/mmv1/templates/terraform/pre_update/datastream_connection_profile.go.tmpl +++ b/mmv1/templates/terraform/pre_update/datastream_connection_profile.go.tmpl @@ -24,6 +24,9 @@ if d.HasChange("mongodb_profile.0.replica_set") { if d.HasChange("mongodb_profile.0.host_addresses") { updateMask = append(updateMask, "mongodbProfile.hostAddresses") } +if d.HasChange("mongodb_profile.0.additional_options") { + updateMask = append(updateMask, "mongodbProfile.additionalOptions") +} // Override the previous setting of updateMask to include state. // updateMask is a URL parameter but not present in the schema, so ReplaceVars diff --git a/mmv1/third_party/terraform/services/datastream/resource_datastream_connection_profile_test.go b/mmv1/third_party/terraform/services/datastream/resource_datastream_connection_profile_test.go index f09d9bd35552..a608bd6906c7 100644 --- a/mmv1/third_party/terraform/services/datastream/resource_datastream_connection_profile_test.go +++ b/mmv1/third_party/terraform/services/datastream/resource_datastream_connection_profile_test.go @@ -730,7 +730,8 @@ resource "google_datastream_connection_profile" "default" { secret_manager_stored_client_key = google_secret_manager_secret_version.client_key_secret_version.id } additional_options = { - readPreference = "secondary" + readPreference = "primary" // <-- Changed + connectTimeoutMS = "5000" // <-- Added } standard_connection_format { direct_connection = true From 97f8bc5bb86b33c16d6d8a1a0480c479d26eaf4f Mon Sep 17 00:00:00 2001 From: Mauricio Alvarez Leon <65101411+BBBmau@users.noreply.github.com> Date: Thu, 9 Jul 2026 10:23:03 -0700 Subject: [PATCH 69/78] =?UTF-8?q?compute:=20add=20list=20resources=20(batc?= =?UTF-8?q?h=204=20=E2=80=94=20proxies,=20URL=20maps,=20snapshots)=20(#178?= =?UTF-8?q?81)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mmv1/products/compute/InstantSnapshot.yaml | 1 + mmv1/products/compute/RegionInstantSnapshot.yaml | 1 + mmv1/products/compute/RegionTargetHttpProxy.yaml | 1 + mmv1/products/compute/RegionTargetTcpProxy.yaml | 3 +++ mmv1/products/compute/RegionUrlMap.yaml | 1 + mmv1/products/compute/RolloutPlan.yaml | 1 + mmv1/products/compute/TargetGrpcProxy.yaml | 1 + mmv1/products/compute/TargetHttpProxy.yaml | 1 + mmv1/products/compute/TargetSslProxy.yaml | 1 + mmv1/products/compute/TargetTcpProxy.yaml | 1 + .../terraform/samples/base_configs/query_test_file.go.tmpl | 2 ++ .../services/compute/region_target_tcp_proxy_basic.tf.tmpl | 6 +++--- 12 files changed, 17 insertions(+), 3 deletions(-) diff --git a/mmv1/products/compute/InstantSnapshot.yaml b/mmv1/products/compute/InstantSnapshot.yaml index 999699ad82bf..1196caf2e523 100644 --- a/mmv1/products/compute/InstantSnapshot.yaml +++ b/mmv1/products/compute/InstantSnapshot.yaml @@ -32,6 +32,7 @@ docs: base_url: 'projects/{{project}}/zones/{{zone}}/instantSnapshots' has_self_link: true immutable: true +generate_list_resource: true timeouts: insert_minutes: 20 update_minutes: 20 diff --git a/mmv1/products/compute/RegionInstantSnapshot.yaml b/mmv1/products/compute/RegionInstantSnapshot.yaml index 0d11b78c83b9..c4fc2d6a4a03 100644 --- a/mmv1/products/compute/RegionInstantSnapshot.yaml +++ b/mmv1/products/compute/RegionInstantSnapshot.yaml @@ -32,6 +32,7 @@ docs: base_url: 'projects/{{project}}/regions/{{region}}/instantSnapshots' has_self_link: true immutable: true +generate_list_resource: true timeouts: insert_minutes: 20 update_minutes: 20 diff --git a/mmv1/products/compute/RegionTargetHttpProxy.yaml b/mmv1/products/compute/RegionTargetHttpProxy.yaml index b42ca9e36f5e..642005cd6a0b 100644 --- a/mmv1/products/compute/RegionTargetHttpProxy.yaml +++ b/mmv1/products/compute/RegionTargetHttpProxy.yaml @@ -27,6 +27,7 @@ has_self_link: true immutable: true datasource_experimental: generate: true +generate_list_resource: true timeouts: insert_minutes: 20 update_minutes: 20 diff --git a/mmv1/products/compute/RegionTargetTcpProxy.yaml b/mmv1/products/compute/RegionTargetTcpProxy.yaml index 3fa907b91ec7..0eaba50c9aa7 100644 --- a/mmv1/products/compute/RegionTargetTcpProxy.yaml +++ b/mmv1/products/compute/RegionTargetTcpProxy.yaml @@ -25,6 +25,7 @@ references: base_url: projects/{{project}}/regions/{{region}}/targetTcpProxies immutable: true has_self_link: true +generate_list_resource: true id_format: projects/{{project}}/regions/{{region}}/targetTcpProxies/{{name}} sweeper: url_substitutions: @@ -39,6 +40,8 @@ samples: primary_resource_id: default steps: - name: region_target_tcp_proxy_basic + vars: + region: us-central1 resource_id_vars: health_check_name: health-check region_backend_service_name: backend-service diff --git a/mmv1/products/compute/RegionUrlMap.yaml b/mmv1/products/compute/RegionUrlMap.yaml index 2116ed3f3c4e..d00a9bacb482 100644 --- a/mmv1/products/compute/RegionUrlMap.yaml +++ b/mmv1/products/compute/RegionUrlMap.yaml @@ -21,6 +21,7 @@ description: | docs: base_url: 'projects/{{project}}/regions/{{region}}/urlMaps' has_self_link: true +generate_list_resource: true timeouts: insert_minutes: 20 update_minutes: 20 diff --git a/mmv1/products/compute/RolloutPlan.yaml b/mmv1/products/compute/RolloutPlan.yaml index 5737e7ea65fc..79b3b9a7622e 100644 --- a/mmv1/products/compute/RolloutPlan.yaml +++ b/mmv1/products/compute/RolloutPlan.yaml @@ -24,6 +24,7 @@ references: api: 'https://cloud.google.com/compute/docs/reference/rest/beta/rolloutPlans' base_url: 'projects/{{project}}/global/rolloutPlans' has_self_link: true +generate_list_resource: true immutable: true create_url: 'projects/{{project}}/global/rolloutPlans' create_verb: 'POST' diff --git a/mmv1/products/compute/TargetGrpcProxy.yaml b/mmv1/products/compute/TargetGrpcProxy.yaml index c9774e01c784..34ab40a94367 100644 --- a/mmv1/products/compute/TargetGrpcProxy.yaml +++ b/mmv1/products/compute/TargetGrpcProxy.yaml @@ -27,6 +27,7 @@ docs: base_url: 'projects/{{project}}/global/targetGrpcProxies' has_self_link: true update_verb: 'PATCH' +generate_list_resource: true timeouts: insert_minutes: 20 update_minutes: 20 diff --git a/mmv1/products/compute/TargetHttpProxy.yaml b/mmv1/products/compute/TargetHttpProxy.yaml index 9f3afb92dace..2f8728a2c9ba 100644 --- a/mmv1/products/compute/TargetHttpProxy.yaml +++ b/mmv1/products/compute/TargetHttpProxy.yaml @@ -27,6 +27,7 @@ has_self_link: true immutable: true datasource_experimental: generate: true +generate_list_resource: true timeouts: insert_minutes: 20 update_minutes: 20 diff --git a/mmv1/products/compute/TargetSslProxy.yaml b/mmv1/products/compute/TargetSslProxy.yaml index 5872df473d47..62c3c0509a6d 100644 --- a/mmv1/products/compute/TargetSslProxy.yaml +++ b/mmv1/products/compute/TargetSslProxy.yaml @@ -26,6 +26,7 @@ docs: base_url: 'projects/{{project}}/global/targetSslProxies' has_self_link: true immutable: true +generate_list_resource: true timeouts: insert_minutes: 20 update_minutes: 20 diff --git a/mmv1/products/compute/TargetTcpProxy.yaml b/mmv1/products/compute/TargetTcpProxy.yaml index 34a315638fb3..0105842a72ad 100644 --- a/mmv1/products/compute/TargetTcpProxy.yaml +++ b/mmv1/products/compute/TargetTcpProxy.yaml @@ -24,6 +24,7 @@ references: base_url: projects/{{project}}/global/targetTcpProxies immutable: true has_self_link: true +generate_list_resource: true collection_url_key: items kind: compute#targetTcpProxy id_format: projects/{{project}}/global/targetTcpProxies/{{name}} diff --git a/mmv1/templates/terraform/samples/base_configs/query_test_file.go.tmpl b/mmv1/templates/terraform/samples/base_configs/query_test_file.go.tmpl index 0ce1857eca9f..e5534f0d83ee 100644 --- a/mmv1/templates/terraform/samples/base_configs/query_test_file.go.tmpl +++ b/mmv1/templates/terraform/samples/base_configs/query_test_file.go.tmpl @@ -63,6 +63,7 @@ func TestAcc{{ $.ResourceName }}ListQuery_generated(t *testing.T) { {{- end }} {{- range $scope := $.ListScopeProperties }} {{- $n := underscore $scope.Name }} + {{- if not (index $step.TestContextVars $n) }} {{- if or (eq $n "region") (eq $n "location") }} "{{$n}}": envvar.GetTestRegionFromEnv(), {{- else if eq $n "zone" }} @@ -72,6 +73,7 @@ func TestAcc{{ $.ResourceName }}ListQuery_generated(t *testing.T) { "project": envvar.GetTestProjectFromEnv(), {{- end }} {{- end }} + {{- end }} {{- end }} "random_suffix": randomSuffix, } diff --git a/mmv1/templates/terraform/samples/services/compute/region_target_tcp_proxy_basic.tf.tmpl b/mmv1/templates/terraform/samples/services/compute/region_target_tcp_proxy_basic.tf.tmpl index 7f5968a976d3..f0fe78be1b70 100644 --- a/mmv1/templates/terraform/samples/services/compute/region_target_tcp_proxy_basic.tf.tmpl +++ b/mmv1/templates/terraform/samples/services/compute/region_target_tcp_proxy_basic.tf.tmpl @@ -1,6 +1,6 @@ resource "google_compute_region_target_tcp_proxy" "{{$.PrimaryResourceId}}" { name = "{{index $.ResourceIdVars "region_target_tcp_proxy_name"}}" - region = "europe-west4" + region = "{{index $.Vars "region"}}" backend_service = google_compute_region_backend_service.default.id } @@ -8,7 +8,7 @@ resource "google_compute_region_backend_service" "default" { name = "{{index $.ResourceIdVars "region_backend_service_name"}}" protocol = "TCP" timeout_sec = 10 - region = "europe-west4" + region = "{{index $.Vars "region"}}" health_checks = [google_compute_region_health_check.default.id] load_balancing_scheme = "INTERNAL_MANAGED" @@ -16,7 +16,7 @@ resource "google_compute_region_backend_service" "default" { resource "google_compute_region_health_check" "default" { name = "{{index $.ResourceIdVars "health_check_name"}}" - region = "europe-west4" + region = "{{index $.Vars "region"}}" timeout_sec = 1 check_interval_sec = 1 tcp_health_check { From 5159b3edd9d2415c4ab74899c110283132ce24f0 Mon Sep 17 00:00:00 2001 From: Vaibhav Rau Date: Thu, 9 Jul 2026 12:34:37 -0500 Subject: [PATCH 70/78] Add list resource for google_dns_managed_zone (#18133) Co-authored-by: Mauricio Alvarez Leon <65101411+BBBmau@users.noreply.github.com> --- .../dns/list_google_dns_managed_zone.go | 163 ++++++++++++++++++ .../dns/list_google_dns_managed_zone_test.go | 112 ++++++++++++ .../google_dns_managed_zone.html.markdown | 50 ++++++ 3 files changed, 325 insertions(+) create mode 100644 mmv1/third_party/terraform/services/dns/list_google_dns_managed_zone.go create mode 100644 mmv1/third_party/terraform/services/dns/list_google_dns_managed_zone_test.go create mode 100644 mmv1/third_party/terraform/website/docs/list-resources/google_dns_managed_zone.html.markdown diff --git a/mmv1/third_party/terraform/services/dns/list_google_dns_managed_zone.go b/mmv1/third_party/terraform/services/dns/list_google_dns_managed_zone.go new file mode 100644 index 000000000000..c038fdc7a85e --- /dev/null +++ b/mmv1/third_party/terraform/services/dns/list_google_dns_managed_zone.go @@ -0,0 +1,163 @@ +package dns + +import ( + "context" + "errors" + "fmt" + + frameworkdiag "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/list" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + + "github.com/hashicorp/terraform-provider-google/google/registry" + "github.com/hashicorp/terraform-provider-google/google/tpgresource" + transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" +) + +var errStreamClosed = errors.New("stream closed") + +func init() { + registry.FrameworkListResource{ + Name: "google_dns_managed_zone", + ProductName: "dns", + Func: NewGoogleDnsManagedZoneListResource, + }.Register() +} + +type GoogleDnsManagedZoneResource struct { + tpgresource.ListResourceMetadata +} + +type GoogleDnsManagedZoneListModel struct { + Project types.String `tfsdk:"project"` +} + +func NewGoogleDnsManagedZoneListResource() list.ListResource { + listR := &GoogleDnsManagedZoneResource{} + listR.TypeName = "google_dns_managed_zone" + listR.SDKv2Resource = ResourceDNSManagedZone() + listR.ListConfigFields = []tpgresource.ListConfigField{{Name: "project", Kind: tpgresource.ListConfigKindString, Optional: true}} + return listR +} + +func (listR *GoogleDnsManagedZoneResource) List(ctx context.Context, req list.ListRequest, stream *list.ListResultsStream) { + var data GoogleDnsManagedZoneListModel + diags := req.Config.Get(ctx, &data) + + if diags.HasError() { + stream.Results = list.ListResultsStreamDiagnostics(diags) + return + } + + if listR.Client == nil { + diags = append(diags, frameworkdiag.NewErrorDiagnostic( + "provider not configured", + "The Google provider client is not available; ensure the provider is configured.", + )) + stream.Results = list.ListResultsStreamDiagnostics(diags) + return + } + + tempData := listR.SDKv2Resource.Data(&terraform.InstanceState{}) + if !data.Project.IsNull() && !data.Project.IsUnknown() { + if err := tempData.Set("project", data.Project.ValueString()); err != nil { + diags.AddError("Failed to set project", err.Error()) + stream.Results = list.ListResultsStreamDiagnostics(diags) + return + } + } + + project, err := tpgresource.GetProject(tempData, listR.Client) + if err != nil { + diags.AddError("Failed to resolve project", err.Error()) + stream.Results = list.ListResultsStreamDiagnostics(diags) + return + } + + stream.Results = func(push func(list.ListResult) bool) { + var streamDiags frameworkdiag.Diagnostics + + err := ListDnsManagedZones(listR.Client, project, func(rd *schema.ResourceData) error { + result := req.NewListResult(ctx) + + if err := listR.SetResult(ctx, req.IncludeResource, &result, rd, "name"); err != nil { + streamDiags.AddError("Failed to set result", err.Error()) + return err + } + + if !push(result) { + // A closed stream means the consumer has enough results; stop cleanly. + return errStreamClosed + } + return nil + }) + + if err != nil { + // Stream closure is expected and should not be reported as an API error. + if errors.Is(err, errStreamClosed) { + return + } + streamDiags.AddError("API Error listing DNS managed zones", fmt.Sprintf("Failed to list DNS managed zones in project %q: %v", project, err)) + result := req.NewListResult(ctx) + result.Diagnostics = streamDiags + push(result) + return + } + + if streamDiags.HasError() { + stream.Results = list.ListResultsStreamDiagnostics(streamDiags) + } + } +} + +func ListDnsManagedZones(config *transport_tpg.Config, project string, callback func(*schema.ResourceData) error) error { + if config == nil { + return fmt.Errorf("provider client is not configured") + } + + managedZoneSchema := ResourceDNSManagedZone() + tempData := managedZoneSchema.Data(&terraform.InstanceState{}) + if project != "" { + if err := tempData.Set("project", project); err != nil { + return fmt.Errorf("error setting project on temporary resource data: %w", err) + } + } + + userAgent, err := tpgresource.GenerateUserAgentString(tempData, config.UserAgent) + if err != nil { + return err + } + + url, err := tpgresource.ReplaceVars(tempData, config, "{{DNSBasePath}}projects/{{project}}/managedZones") + if err != nil { + return err + } + + return transport_tpg.ListPages(transport_tpg.ListPagesOptions{ + Config: config, + TempData: tempData, + Resource: managedZoneSchema, + ListURL: url, + Filter: "", + ItemName: "managedZones", + UserAgent: userAgent, + Flattener: func(res map[string]interface{}, d *schema.ResourceData, config *transport_tpg.Config) error { + name, _ := res["name"].(string) + + d.SetId(fmt.Sprintf("projects/%s/managedZones/%s", project, name)) + + if err := ResourceDNSManagedZoneFlatten(d, config, res, config, project, "", "", "", nil); err != nil { + return err + } + + if err := d.Set("project", project); err != nil { + return fmt.Errorf("error setting project: %w", err) + } + + return nil + }, + Callback: callback, + }) +} diff --git a/mmv1/third_party/terraform/services/dns/list_google_dns_managed_zone_test.go b/mmv1/third_party/terraform/services/dns/list_google_dns_managed_zone_test.go new file mode 100644 index 000000000000..6f9ab89f7e33 --- /dev/null +++ b/mmv1/third_party/terraform/services/dns/list_google_dns_managed_zone_test.go @@ -0,0 +1,112 @@ +package dns_test + +import ( + "fmt" + "testing" + "time" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/querycheck" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-plugin-testing/tfversion" + + "github.com/hashicorp/terraform-provider-google/google/acctest" + "github.com/hashicorp/terraform-provider-google/google/envvar" + tpgdns "github.com/hashicorp/terraform-provider-google/google/services/dns" +) + +func TestAccDnsManagedZoneListResource_queryIdentity(t *testing.T) { + t.Parallel() + + zoneName := fmt.Sprintf("tf-test-%s", acctest.RandString(t, 10)) + dnsName := fmt.Sprintf("tf-test-%s.hashicorptest.com.", acctest.RandString(t, 10)) + project := envvar.GetTestProjectFromEnv() + t.Logf("Using project %s for testing", project) + + acctest.VcrTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + Steps: []resource.TestStep{ + { + Config: testAccDnsManagedZoneListResource_basic(zoneName, dnsName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_dns_managed_zone.foobar", "project", project), + resource.TestCheckResourceAttr("google_dns_managed_zone.foobar", "name", zoneName), + resource.TestCheckResourceAttr("google_dns_managed_zone.foobar", "dns_name", dnsName), + testAccCheckDnsManagedZoneVisibleInList(t, project, zoneName), + ), + }, + { + Query: true, + Config: testAccDnsManagedZoneListQuery(project), + QueryResultChecks: []querycheck.QueryResultCheck{ + querycheck.ExpectLengthAtLeast("google_dns_managed_zone.all", 1), + querycheck.ExpectIdentity("google_dns_managed_zone.all", map[string]knownvalue.Check{ + "name": knownvalue.StringExact(zoneName), + "project": knownvalue.StringExact(project), + }), + }, + }, + }, + }) +} + +func testAccCheckDnsManagedZoneVisibleInList(t *testing.T, project, zoneName string) resource.TestCheckFunc { + return func(_ *terraform.State) error { + config := acctest.GoogleProviderConfig(t) + + observedNames := make([]string, 0) + err := resource.Retry(30*time.Second, func() *resource.RetryError { + resp, err := tpgdns.NewClient(config, config.UserAgent).ManagedZones.List(project).Do() + if err != nil { + return resource.RetryableError(fmt.Errorf("error listing managed zones for project %q: %w", project, err)) + } + + observedNames = observedNames[:0] + for _, zone := range resp.ManagedZones { + if zone == nil { + continue + } + observedNames = append(observedNames, zone.Name) + if zone.Name == zoneName { + return nil + } + } + + return resource.RetryableError(fmt.Errorf("managed zone %q not visible yet; currently observed zones: %v", zoneName, observedNames)) + }) + if err != nil { + return fmt.Errorf("managed zone %q did not become query-visible before list step in project %q; last observed zones: %v; retry error: %w", zoneName, project, observedNames, err) + } + + return nil + } +} + +func testAccDnsManagedZoneListResource_basic(name, dnsName string) string { + return fmt.Sprintf(` +resource "google_dns_managed_zone" "foobar" { + name = %q + dns_name = %q + visibility = "public" +} +`, name, dnsName) +} + +func testAccDnsManagedZoneListQuery(project string) string { + return fmt.Sprintf(` +provider "google" {} + +list "google_dns_managed_zone" "all" { + provider = google + limit = 1000 + config { + project = %q + } +} +`, project) +} diff --git a/mmv1/third_party/terraform/website/docs/list-resources/google_dns_managed_zone.html.markdown b/mmv1/third_party/terraform/website/docs/list-resources/google_dns_managed_zone.html.markdown new file mode 100644 index 000000000000..9f84bd674b95 --- /dev/null +++ b/mmv1/third_party/terraform/website/docs/list-resources/google_dns_managed_zone.html.markdown @@ -0,0 +1,50 @@ +--- +subcategory: "Cloud DNS" +page_title: "google_dns_managed_zone" +description: |- + Lists DNS managed zones in a project. +--- + +# google_dns_managed_zone (list) + +Lists **DNS managed zones** in a Google Cloud project for use with +[`terraform query`](https://developer.hashicorp.com/terraform/cli/commands/query) and +**`.tfquery.hcl`** files. Results correspond to existing +[`google_dns_managed_zone`](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/dns_managed_zone) +managed resources. + +For how list resources work in this provider, file layout, Terraform version requirements, and +shared `list` block arguments, refer to the guide +[Use list resources with terraform query (Google Cloud provider)](https://registry.terraform.io/providers/hashicorp/google/latest/docs/guides/using_list_resources_with_terraform_query). + +## Example + +```hcl +list "google_dns_managed_zone" "all" { + provider = google + + config { + # Optional. Defaults to the provider project when omitted. + # project = "my-project" + } +} +``` + +Run `terraform query` from the directory that contains the `.tfquery.hcl` file. + +## Configuration (`config` block) + +* `project` - (Optional) Project ID whose DNS managed zones are listed. If unset, the provider's + configured default project is used. + +## Results + +By default each result includes **resource identity** for `google_dns_managed_zone` (see +[Resource identity](https://developer.hashicorp.com/terraform/language/resources/identities)): + +* `name` - Managed zone name. +* `project` - Project ID. + +With `include_resource = true` on the `list` block, results also include the full resource-style +attributes documented for the managed +[`google_dns_managed_zone` resource](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/dns_managed_zone#attributes-reference). \ No newline at end of file From faca318e2372217496af7d604bdd8adb3d809d98 Mon Sep 17 00:00:00 2001 From: Nayyar Date: Thu, 9 Jul 2026 23:22:32 +0530 Subject: [PATCH 71/78] Reject empty segments in JWT validators (#18082) Co-authored-by: Nayyar --- .../fwvalidators/framework_validators.go | 12 ++- .../fwvalidators/framework_validators_test.go | 94 +++++++++++++++++++ .../provider/provider_internal_test.go | 14 +++ .../terraform/provider/provider_validators.go | 8 +- 4 files changed, 126 insertions(+), 2 deletions(-) diff --git a/mmv1/third_party/terraform/fwvalidators/framework_validators.go b/mmv1/third_party/terraform/fwvalidators/framework_validators.go index eed3a32e98f9..1e852a5030f0 100644 --- a/mmv1/third_party/terraform/fwvalidators/framework_validators.go +++ b/mmv1/third_party/terraform/fwvalidators/framework_validators.go @@ -249,8 +249,18 @@ func (v jwtValidator) ValidateString(ctx context.Context, request validator.Stri return } - // Check that each part is base64 encoded + // Check that each part is non-empty and base64url encoded. An empty + // segment decodes without error, so a token like ".." would otherwise + // be accepted as a valid JWT. for i, part := range parts { + if part == "" { + response.Diagnostics.AddAttributeError( + request.Path, + "Invalid JWT Format", + fmt.Sprintf("Part %d of JWT must not be empty (expected header.payload.signature)", i+1), + ) + continue + } if _, err := base64.RawURLEncoding.DecodeString(part); err != nil { response.Diagnostics.AddAttributeError( request.Path, diff --git a/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go b/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go index 8d8a285584fd..10dea9678c4d 100644 --- a/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go +++ b/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go @@ -2,6 +2,7 @@ package fwvalidators_test import ( "context" + "strings" "testing" "time" @@ -161,6 +162,99 @@ func TestServiceAccountEmailValidator(t *testing.T) { } } +func TestJWTValidator(t *testing.T) { + t.Parallel() + + type testCase struct { + value types.String + expectError bool + errorContains string + } + + tests := map[string]testCase{ + "valid jwt": { + value: types.StringValue("eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxMjMifQ.c2ln"), + expectError: false, + }, + "empty string": { + value: types.StringValue(""), + expectError: true, + errorContains: "JWT token must not be empty", + }, + "wrong number of segments": { + value: types.StringValue("header.payload"), + expectError: true, + errorContains: "JWT token must have 3 parts separated by dots (header.payload.signature)", + }, + "all segments empty": { + value: types.StringValue(".."), + expectError: true, + errorContains: "Part 1 of JWT must not be empty (expected header.payload.signature)", + }, + "empty payload segment": { + value: types.StringValue("ab..cd"), + expectError: true, + errorContains: "Part 2 of JWT must not be empty (expected header.payload.signature)", + }, + "empty signature segment": { + value: types.StringValue("eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxMjMifQ."), + expectError: true, + errorContains: "Part 3 of JWT must not be empty (expected header.payload.signature)", + }, + "invalid base64 segment": { + value: types.StringValue("not base64.eyJzdWIiOiIxMjMifQ.c2ln"), + expectError: true, + errorContains: "Part 1 of JWT is not valid base64", + }, + "null value": { + value: types.StringNull(), + expectError: false, + }, + "unknown value": { + value: types.StringUnknown(), + expectError: false, + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + t.Parallel() + + request := validator.StringRequest{ + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + ConfigValue: test.value, + } + response := validator.StringResponse{} + v := fwvalidators.JWTValidator() + + v.ValidateString(context.Background(), request, &response) + + if test.expectError && !response.Diagnostics.HasError() { + t.Errorf("expected error, got none for value: %q", test.value.ValueString()) + } + + if !test.expectError && response.Diagnostics.HasError() { + t.Errorf("got unexpected error for value: %q: %s", test.value.ValueString(), response.Diagnostics.Errors()) + } + + if test.errorContains != "" { + foundError := false + for _, err := range response.Diagnostics.Errors() { + if strings.Contains(err.Detail(), test.errorContains) { + foundError = true + break + } + } + if !foundError { + t.Errorf("expected error containing %q, got %v", test.errorContains, response.Diagnostics.Errors()) + } + } + }) + } +} + func TestBoundedDuration(t *testing.T) { t.Parallel() diff --git a/mmv1/third_party/terraform/provider/provider_internal_test.go b/mmv1/third_party/terraform/provider/provider_internal_test.go index 03a02a4d808a..ae1d3d5b57b5 100644 --- a/mmv1/third_party/terraform/provider/provider_internal_test.go +++ b/mmv1/third_party/terraform/provider/provider_internal_test.go @@ -113,6 +113,20 @@ func TestProvider_ValidateJWT(t *testing.T) { errors.New("\"\" is not a valid JWT format"), }, }, + "a JWT with an empty segment is rejected": { + ConfigValue: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..c2ln", + ExpectedErrors: []error{ + errors.New("part 2 of JWT must not be empty (expected header.payload.signature)"), + }, + }, + "a JWT with all empty segments is rejected": { + ConfigValue: "..", + ExpectedErrors: []error{ + errors.New("part 1 of JWT must not be empty (expected header.payload.signature)"), + errors.New("part 2 of JWT must not be empty (expected header.payload.signature)"), + errors.New("part 3 of JWT must not be empty (expected header.payload.signature)"), + }, + }, "unconfigured value is not valid": { ValueNotProvided: true, ExpectedErrors: []error{ diff --git a/mmv1/third_party/terraform/provider/provider_validators.go b/mmv1/third_party/terraform/provider/provider_validators.go index c0df520da106..973a6cb6e130 100644 --- a/mmv1/third_party/terraform/provider/provider_validators.go +++ b/mmv1/third_party/terraform/provider/provider_validators.go @@ -69,8 +69,14 @@ func ValidateJWT(v interface{}, k string) (warnings []string, errors []error) { return } - // Check that each part is base64 encoded + // Check that each part is non-empty and base64url encoded. An empty + // segment decodes without error, so a token like ".." would otherwise + // be accepted as a valid JWT. for i, part := range parts { + if part == "" { + errors = append(errors, fmt.Errorf("part %d of JWT must not be empty (expected header.payload.signature)", i+1)) + continue + } if _, err := base64.RawURLEncoding.DecodeString(part); err != nil { errors = append(errors, fmt.Errorf("part %d of JWT is not valid base64: %v", i+1, err)) } From ed14a1adbbacbb038d5099669a52f2c501c8ad68 Mon Sep 17 00:00:00 2001 From: virgulati2003 <108871727+virgulati2003@users.noreply.github.com> Date: Fri, 10 Jul 2026 00:44:50 +0530 Subject: [PATCH 72/78] Add singular/plural Exascale DB Storage Vault data sources and update VM Clusters data source (#18238) Co-authored-by: Cameron Thornton --- ...ource_oracle_database_cloud_vm_clusters.go | 25 ++-- ...acle_database_exascale_db_storage_vault.go | 49 ++++++++ ...database_exascale_db_storage_vault_test.go | 60 +++++++++ ...cle_database_exascale_db_storage_vaults.go | 118 ++++++++++++++++++ ...atabase_exascale_db_storage_vaults_test.go | 59 +++++++++ ...se_exascale_db_storage_vault.html.markdown | 37 ++++++ ...e_exascale_db_storage_vaults.html.markdown | 36 ++++++ 7 files changed, 372 insertions(+), 12 deletions(-) create mode 100644 mmv1/third_party/terraform/services/oracledatabase/data_source_oracle_database_exascale_db_storage_vault.go create mode 100644 mmv1/third_party/terraform/services/oracledatabase/data_source_oracle_database_exascale_db_storage_vault_test.go create mode 100644 mmv1/third_party/terraform/services/oracledatabase/data_source_oracle_database_exascale_db_storage_vaults.go create mode 100644 mmv1/third_party/terraform/services/oracledatabase/data_source_oracle_database_exascale_db_storage_vaults_test.go create mode 100644 mmv1/third_party/terraform/website/docs/d/oracle_database_exascale_db_storage_vault.html.markdown create mode 100644 mmv1/third_party/terraform/website/docs/d/oracle_database_exascale_db_storage_vaults.html.markdown diff --git a/mmv1/third_party/terraform/services/oracledatabase/data_source_oracle_database_cloud_vm_clusters.go b/mmv1/third_party/terraform/services/oracledatabase/data_source_oracle_database_cloud_vm_clusters.go index 442d12886e65..2276dadfce44 100644 --- a/mmv1/third_party/terraform/services/oracledatabase/data_source_oracle_database_cloud_vm_clusters.go +++ b/mmv1/third_party/terraform/services/oracledatabase/data_source_oracle_database_cloud_vm_clusters.go @@ -96,18 +96,19 @@ func flattenOracleDatabaseCloudVmClusters(v interface{}, d *schema.ResourceData, for _, raw := range l { original := raw.(map[string]interface{}) transformed = append(transformed, map[string]interface{}{ - "name": flattenOracleDatabaseCloudVmClusterName(original["name"], d, config), - "exadata_infrastructure": flattenOracleDatabaseCloudVmClusterExadataInfrastructure(original["exadataInfrastructure"], d, config), - "display_name": flattenOracleDatabaseCloudVmClusterDisplayName(original["displayName"], d, config), - "gcp_oracle_zone": flattenOracleDatabaseCloudVmClusterGcpOracleZone(original["gcpOracleZone"], d, config), - "properties": flattenOracleDatabaseCloudVmClusterProperties(original["properties"], d, config), - "labels": flattenOracleDatabaseCloudVmClusterLabels(original["labels"], d, config), - "create_time": flattenOracleDatabaseCloudVmClusterCreateTime(original["createTime"], d, config), - "cidr": flattenOracleDatabaseCloudVmClusterCidr(original["cidr"], d, config), - "backup_subnet_cidr": flattenOracleDatabaseCloudVmClusterBackupSubnetCidr(original["backupSubnetCidr"], d, config), - "network": flattenOracleDatabaseCloudVmClusterNetwork(original["network"], d, config), - "terraform_labels": flattenOracleDatabaseCloudVmClusterTerraformLabels(original["labels"], d, config), - "effective_labels": flattenOracleDatabaseCloudVmClusterEffectiveLabels(original["labels"], d, config), + "name": flattenOracleDatabaseCloudVmClusterName(original["name"], d, config), + "exadata_infrastructure": flattenOracleDatabaseCloudVmClusterExadataInfrastructure(original["exadataInfrastructure"], d, config), + "display_name": flattenOracleDatabaseCloudVmClusterDisplayName(original["displayName"], d, config), + "gcp_oracle_zone": flattenOracleDatabaseCloudVmClusterGcpOracleZone(original["gcpOracleZone"], d, config), + "properties": flattenOracleDatabaseCloudVmClusterProperties(original["properties"], d, config), + "labels": flattenOracleDatabaseCloudVmClusterLabels(original["labels"], d, config), + "create_time": flattenOracleDatabaseCloudVmClusterCreateTime(original["createTime"], d, config), + "cidr": flattenOracleDatabaseCloudVmClusterCidr(original["cidr"], d, config), + "backup_subnet_cidr": flattenOracleDatabaseCloudVmClusterBackupSubnetCidr(original["backupSubnetCidr"], d, config), + "network": flattenOracleDatabaseCloudVmClusterNetwork(original["network"], d, config), + "exascale_db_storage_vault": flattenOracleDatabaseCloudVmClusterExascaleDbStorageVault(original["exascaleDbStorageVault"], d, config), + "terraform_labels": flattenOracleDatabaseCloudVmClusterTerraformLabels(original["labels"], d, config), + "effective_labels": flattenOracleDatabaseCloudVmClusterEffectiveLabels(original["labels"], d, config), }) } return transformed diff --git a/mmv1/third_party/terraform/services/oracledatabase/data_source_oracle_database_exascale_db_storage_vault.go b/mmv1/third_party/terraform/services/oracledatabase/data_source_oracle_database_exascale_db_storage_vault.go new file mode 100644 index 000000000000..e3bcef5d36c3 --- /dev/null +++ b/mmv1/third_party/terraform/services/oracledatabase/data_source_oracle_database_exascale_db_storage_vault.go @@ -0,0 +1,49 @@ +package oracledatabase + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-provider-google/google/registry" + "github.com/hashicorp/terraform-provider-google/google/tpgresource" + transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" +) + +func DataSourceOracleDatabaseExascaleDbStorageVault() *schema.Resource { + dsSchema := tpgresource.DatasourceSchemaFromResourceSchema(ResourceOracleDatabaseExascaleDbStorageVault().Schema) + tpgresource.AddRequiredFieldsToSchema(dsSchema, "location", "exascale_db_storage_vault_id") + tpgresource.AddOptionalFieldsToSchema(dsSchema, "project") + return &schema.Resource{ + Read: dataSourceOracleDatabaseExascaleDbStorageVaultRead, + Schema: dsSchema, + } +} + +func dataSourceOracleDatabaseExascaleDbStorageVaultRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*transport_tpg.Config) + + id, err := tpgresource.ReplaceVars(d, config, "projects/{{project}}/locations/{{location}}/exascaleDbStorageVaults/{{exascale_db_storage_vault_id}}") + if err != nil { + return fmt.Errorf("Error constructing id: %s", err) + } + d.SetId(id) + err = resourceOracleDatabaseExascaleDbStorageVaultRead(d, meta) + if err != nil { + return err + } + if d.Id() == "" { + return fmt.Errorf("%s not found", id) + } + return nil + + return nil +} + +func init() { + registry.Schema{ + Name: "google_oracle_database_exascale_db_storage_vault", + ProductName: "oracledatabase", + Type: registry.SchemaTypeDataSource, + Schema: DataSourceOracleDatabaseExascaleDbStorageVault(), + }.Register() +} diff --git a/mmv1/third_party/terraform/services/oracledatabase/data_source_oracle_database_exascale_db_storage_vault_test.go b/mmv1/third_party/terraform/services/oracledatabase/data_source_oracle_database_exascale_db_storage_vault_test.go new file mode 100644 index 000000000000..9041fe293394 --- /dev/null +++ b/mmv1/third_party/terraform/services/oracledatabase/data_source_oracle_database_exascale_db_storage_vault_test.go @@ -0,0 +1,60 @@ +package oracledatabase_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-provider-google/google/acctest" + _ "github.com/hashicorp/terraform-provider-google/google/services/oracledatabase" +) + +func TestAccOracleDatabaseExascaleDbStorageVaultDataSource_basic(t *testing.T) { + t.Parallel() + + vaultId := fmt.Sprintf("ofake-tf-test-vault-ds-%s", acctest.RandString(t, 10)) + + context := map[string]interface{}{ + "vault_id": vaultId, + "project": "oci-terraform-testing-prod", + } + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + Steps: []resource.TestStep{ + { + Config: testAccOracleDatabaseExascaleDbStorageVaultDataSource_basic(context), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("data.google_oracle_database_exascale_db_storage_vault.ds_vault", "name"), + resource.TestCheckResourceAttrSet("data.google_oracle_database_exascale_db_storage_vault.ds_vault", "display_name"), + resource.TestCheckResourceAttr("data.google_oracle_database_exascale_db_storage_vault.ds_vault", "display_name", fmt.Sprintf("%s displayname", vaultId)), + resource.TestCheckResourceAttr("data.google_oracle_database_exascale_db_storage_vault.ds_vault", "properties.0.exascale_db_storage_details.0.total_size_gbs", "512"), + ), + }, + }, + }) +} + +func testAccOracleDatabaseExascaleDbStorageVaultDataSource_basic(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_oracle_database_exascale_db_storage_vault" "vault" { + exascale_db_storage_vault_id = "%{vault_id}" + display_name = "%{vault_id} displayname" + location = "us-east4" + project = "%{project}" + properties { + exascale_db_storage_details { + total_size_gbs = 512 + } + } + deletion_protection = false +} + +data "google_oracle_database_exascale_db_storage_vault" "ds_vault" { + exascale_db_storage_vault_id = google_oracle_database_exascale_db_storage_vault.vault.exascale_db_storage_vault_id + location = google_oracle_database_exascale_db_storage_vault.vault.location + project = google_oracle_database_exascale_db_storage_vault.vault.project +} +`, context) +} diff --git a/mmv1/third_party/terraform/services/oracledatabase/data_source_oracle_database_exascale_db_storage_vaults.go b/mmv1/third_party/terraform/services/oracledatabase/data_source_oracle_database_exascale_db_storage_vaults.go new file mode 100644 index 000000000000..5e3bd92b8d20 --- /dev/null +++ b/mmv1/third_party/terraform/services/oracledatabase/data_source_oracle_database_exascale_db_storage_vaults.go @@ -0,0 +1,118 @@ +package oracledatabase + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-provider-google/google/registry" + "github.com/hashicorp/terraform-provider-google/google/tpgresource" + transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" +) + +func DataSourceOracleDatabaseExascaleDbStorageVaults() *schema.Resource { + dsSchema := map[string]*schema.Schema{ + "project": { + Type: schema.TypeString, + Optional: true, + Description: "The ID of the project in which the dataset is located. If it is not provided, the provider project is used.", + }, + "location": { + Type: schema.TypeString, + Required: true, + Description: "location", + }, + "exascale_db_storage_vaults": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: tpgresource.DatasourceSchemaFromResourceSchema(ResourceOracleDatabaseExascaleDbStorageVault().Schema), + }, + }, + } + return &schema.Resource{ + Read: dataSourceOracleDatabaseExascaleDbStorageVaultsRead, + Schema: dsSchema, + } +} + +func dataSourceOracleDatabaseExascaleDbStorageVaultsRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*transport_tpg.Config) + userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) + if err != nil { + return err + } + + url, err := tpgresource.ReplaceVars(d, config, transport_tpg.BaseUrl(Product, config)+"projects/{{project}}/locations/{{location}}/exascaleDbStorageVaults") + if err != nil { + return fmt.Errorf("Error constructing id: %s", err) + } + + billingProject := "" + project, err := tpgresource.GetProject(d, config) + if err != nil { + return fmt.Errorf("Error fetching project for exascaleDbStorageVaults: %s", err) + } + billingProject = project + if bp, err := tpgresource.GetBillingProject(d, config); err == nil { + billingProject = bp + } + res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "GET", + Project: billingProject, + RawURL: url, + UserAgent: userAgent, + }) + + if err != nil { + return fmt.Errorf("Error reading exascaleDbStorageVaults: %s", err) + } + + if err := d.Set("project", project); err != nil { + return fmt.Errorf("Error setting exascaleDbStorageVaults project: %s", err) + } + + if err := d.Set("exascale_db_storage_vaults", flattenOracleDatabaseExascaleDbStorageVaults(res["exascaleDbStorageVaults"], d, config)); err != nil { + return fmt.Errorf("Error setting exascaleDbStorageVaults: %s", err) + } + + id, err := tpgresource.ReplaceVars(d, config, "projects/{{project}}/locations/{{location}}/exascaleDbStorageVaults") + if err != nil { + return fmt.Errorf("Error constructing id: %s", err) + } + d.SetId(id) + return nil +} + +func flattenOracleDatabaseExascaleDbStorageVaults(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) []map[string]interface{} { + if v == nil { + return nil + } + l := v.([]interface{}) + transformed := make([]map[string]interface{}, 0) + for _, raw := range l { + original := raw.(map[string]interface{}) + transformed = append(transformed, map[string]interface{}{ + "name": flattenOracleDatabaseExascaleDbStorageVaultName(original["name"], d, config), + "display_name": flattenOracleDatabaseExascaleDbStorageVaultDisplayName(original["displayName"], d, config), + "gcp_oracle_zone": flattenOracleDatabaseExascaleDbStorageVaultGcpOracleZone(original["gcpOracleZone"], d, config), + "entitlement_id": flattenOracleDatabaseExascaleDbStorageVaultEntitlementId(original["entitlementId"], d, config), + "properties": flattenOracleDatabaseExascaleDbStorageVaultProperties(original["properties"], d, config), + "labels": flattenOracleDatabaseExascaleDbStorageVaultLabels(original["labels"], d, config), + "create_time": flattenOracleDatabaseExascaleDbStorageVaultCreateTime(original["createTime"], d, config), + "exadata_infrastructure": flattenOracleDatabaseExascaleDbStorageVaultExadataInfrastructure(original["exadataInfrastructure"], d, config), + "terraform_labels": flattenOracleDatabaseExascaleDbStorageVaultTerraformLabels(original["labels"], d, config), + "effective_labels": flattenOracleDatabaseExascaleDbStorageVaultEffectiveLabels(original["labels"], d, config), + }) + } + return transformed +} + +func init() { + registry.Schema{ + Name: "google_oracle_database_exascale_db_storage_vaults", + ProductName: "oracledatabase", + Type: registry.SchemaTypeDataSource, + Schema: DataSourceOracleDatabaseExascaleDbStorageVaults(), + }.Register() +} diff --git a/mmv1/third_party/terraform/services/oracledatabase/data_source_oracle_database_exascale_db_storage_vaults_test.go b/mmv1/third_party/terraform/services/oracledatabase/data_source_oracle_database_exascale_db_storage_vaults_test.go new file mode 100644 index 000000000000..3e4712c01f8a --- /dev/null +++ b/mmv1/third_party/terraform/services/oracledatabase/data_source_oracle_database_exascale_db_storage_vaults_test.go @@ -0,0 +1,59 @@ +package oracledatabase_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-provider-google/google/acctest" + _ "github.com/hashicorp/terraform-provider-google/google/services/oracledatabase" +) + +func TestAccOracleDatabaseExascaleDbStorageVaultsDataSource_basic(t *testing.T) { + t.Parallel() + + vaultId := fmt.Sprintf("ofake-tf-test-vault-ds-%s", acctest.RandString(t, 10)) + + context := map[string]interface{}{ + "vault_id": vaultId, + "project": "oci-terraform-testing-prod", + } + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + Steps: []resource.TestStep{ + { + Config: testAccOracleDatabaseExascaleDbStorageVaultsDataSource_basic(context), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("data.google_oracle_database_exascale_db_storage_vaults.ds_vaults", "exascale_db_storage_vaults.#"), + resource.TestCheckResourceAttrSet("data.google_oracle_database_exascale_db_storage_vaults.ds_vaults", "exascale_db_storage_vaults.0.name"), + resource.TestCheckResourceAttrSet("data.google_oracle_database_exascale_db_storage_vaults.ds_vaults", "exascale_db_storage_vaults.0.display_name"), + resource.TestCheckResourceAttrSet("data.google_oracle_database_exascale_db_storage_vaults.ds_vaults", "exascale_db_storage_vaults.0.properties.#"), + ), + }, + }, + }) +} + +func testAccOracleDatabaseExascaleDbStorageVaultsDataSource_basic(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_oracle_database_exascale_db_storage_vault" "vault" { + exascale_db_storage_vault_id = "%{vault_id}" + display_name = "%{vault_id} displayname" + location = "us-east4" + project = "%{project}" + properties { + exascale_db_storage_details { + total_size_gbs = 512 + } + } + deletion_protection = false +} + +data "google_oracle_database_exascale_db_storage_vaults" "ds_vaults" { + location = google_oracle_database_exascale_db_storage_vault.vault.location + project = google_oracle_database_exascale_db_storage_vault.vault.project +} +`, context) +} diff --git a/mmv1/third_party/terraform/website/docs/d/oracle_database_exascale_db_storage_vault.html.markdown b/mmv1/third_party/terraform/website/docs/d/oracle_database_exascale_db_storage_vault.html.markdown new file mode 100644 index 000000000000..57ab43ec26be --- /dev/null +++ b/mmv1/third_party/terraform/website/docs/d/oracle_database_exascale_db_storage_vault.html.markdown @@ -0,0 +1,37 @@ +--- +subcategory: "Oracle Database" +description: |- + Get information about an ExascaleDbStorageVault. +--- + +# google_oracle_database_exascale_db_storage_vault + +Get information about an ExascaleDbStorageVault. + +For more information see the +[API](https://cloud.google.com/oracle/database/docs/reference/rest/v1/projects.locations.exascaleDbStorageVaults). + +## Example Usage + +```hcl +data "google_oracle_database_exascale_db_storage_vault" "my-vault"{ + location = "us-east4" + exascale_db_storage_vault_id = "vault-id" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `exascale_db_storage_vault_id` - (Required) The ID of the ExascaleDbStorageVault. + +* `location` - (Required) The location of the resource. + +- - - +* `project` - (Optional) The project to which the resource belongs. If it + is not provided, the provider project is used. + +## Attributes Reference + +See [google_oracle_database_exascale_db_storage_vault](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/oracle_database_exascale_db_storage_vault#argument-reference) resource for details of the available attributes. diff --git a/mmv1/third_party/terraform/website/docs/d/oracle_database_exascale_db_storage_vaults.html.markdown b/mmv1/third_party/terraform/website/docs/d/oracle_database_exascale_db_storage_vaults.html.markdown new file mode 100644 index 000000000000..4cefe7da77c0 --- /dev/null +++ b/mmv1/third_party/terraform/website/docs/d/oracle_database_exascale_db_storage_vaults.html.markdown @@ -0,0 +1,36 @@ +--- +subcategory: "Oracle Database" +description: |- + List all ExascaleDbStorageVaults. +--- + +# google_oracle_database_exascale_db_storage_vaults + +List all ExascaleDbStorageVaults. + +For more information see the +[API](https://cloud.google.com/oracle/database/docs/reference/rest/v1/projects.locations.exascaleDbStorageVaults). + +## Example Usage + +```hcl +data "google_oracle_database_exascale_db_storage_vaults" "my_vaults"{ + location = "us-east4" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `location` - (Required) The location of the resource. + +- - - +* `project` - (Optional) The project to which the resource belongs. If it + is not provided, the provider project is used. + +## Attributes Reference + +The following attributes are exported: + +* `exascale_db_storage_vaults` - A list of ExascaleDbStorageVaults. See [google_oracle_database_exascale_db_storage_vault](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/oracle_database_exascale_db_storage_vault#argument-reference) resource for details of the available attributes. From c121fdb97aa8781f9f1e29ca3f530e9811d6d1a4 Mon Sep 17 00:00:00 2001 From: Zicong Date: Thu, 9 Jul 2026 13:53:36 -0700 Subject: [PATCH 73/78] feat: Add GKE CustomNodeInit support (#17743) --- .../services/container/node_config.go.tmpl | 109 +++++++ .../resource_container_cluster_meta.yaml.tmpl | 9 + .../resource_container_cluster_test.go.tmpl | 302 ++++++++++++++++++ ...esource_container_node_pool_meta.yaml.tmpl | 6 + .../resource_container_node_pool_test.go.tmpl | 184 +++++++++++ .../docs/r/container_cluster.html.markdown | 14 + .../pkg/services/container/node_config.go | 131 ++++++++ .../resource_container_cluster_tfplan2cai.go | 15 +- .../tgc_next/pkg/tfplan2cai/convert_test.go | 46 +++ .../gke_custom_node_init.tfplan.json | 1 + 10 files changed, 813 insertions(+), 4 deletions(-) create mode 100644 mmv1/third_party/tgc_next/pkg/tfplan2cai/resolvers/gke_custom_node_init.tfplan.json diff --git a/mmv1/third_party/terraform/services/container/node_config.go.tmpl b/mmv1/third_party/terraform/services/container/node_config.go.tmpl index 2acb535a919a..6feed256ae88 100644 --- a/mmv1/third_party/terraform/services/container/node_config.go.tmpl +++ b/mmv1/third_party/terraform/services/container/node_config.go.tmpl @@ -1236,6 +1236,42 @@ func schemaNodeConfig() *schema.Schema { }, }, }, + "custom_node_init": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Description: `The custom node init settings.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "init_script": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Description: `The init script configuration.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "gcs_uri": { + Type: schema.TypeString, + Optional: true, + Description: `The GCS URI of the init script.`, + }, + "gcs_generation": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + Description: `The GCS generation of the init script.`, + }, + "gcp_secret_manager_secret_uri": { + Type: schema.TypeString, + Optional: true, + Description: `The Secret Manager secret URI of the init script.`, + }, + }, + }, + }, + }, + }, + }, }, }, }, @@ -2295,6 +2331,10 @@ func expandLinuxNodeConfig(v interface{}) *container.LinuxNodeConfig { linuxNodeConfig.AccurateTimeConfig = expandAccurateTimeConfig(v) } + if v, ok := cfg["custom_node_init"]; ok { + linuxNodeConfig.CustomNodeInit = expandCustomNodeInit(v) + } + return linuxNodeConfig } @@ -2429,6 +2469,52 @@ func expandAccurateTimeConfig(v interface{}) *container.AccurateTimeConfig { return accurateTimeConfig } +func expandCustomNodeInit(v interface{}) *container.CustomNodeInit { + if v == nil { + return nil + } + ls := v.([]interface{}) + if len(ls) == 0 { + return nil + } + if ls[0] == nil { + return &container.CustomNodeInit{} + } + cfg := ls[0].(map[string]interface{}) + + customNodeInit := &container.CustomNodeInit{} + if v, ok := cfg["init_script"]; ok { + customNodeInit.InitScript = expandInitScript(v) + } + return customNodeInit +} + +func expandInitScript(v interface{}) *container.InitScript { + if v == nil { + return nil + } + ls := v.([]interface{}) + if len(ls) == 0 { + return nil + } + if ls[0] == nil { + return &container.InitScript{} + } + cfg := ls[0].(map[string]interface{}) + + initScript := &container.InitScript{} + if v, ok := cfg["gcs_uri"]; ok { + initScript.GcsUri = v.(string) + } + if v, ok := cfg["gcs_generation"]; ok { + initScript.GcsGeneration = int64(v.(int)) + } + if v, ok := cfg["gcp_secret_manager_secret_uri"]; ok { + initScript.GcpSecretManagerSecretUri = v.(string) + } + return initScript +} + func expandSwapConfig(v interface{}) *container.SwapConfig { if v == nil { return nil @@ -3466,6 +3552,7 @@ func flattenLinuxNodeConfig(c *container.LinuxNodeConfig) []map[string]interface "node_kernel_module_loading": flattenNodeKernelModuleLoading(c.NodeKernelModuleLoading), "swap_config": flattenSwapConfig(c.SwapConfig), "accurate_time_config": flattenAccurateTimeConfig(c.AccurateTimeConfig), + "custom_node_init": flattenCustomNodeInit(c.CustomNodeInit), }) } return result @@ -3481,6 +3568,28 @@ func flattenAccurateTimeConfig(c *container.AccurateTimeConfig) []map[string]int return result } +func flattenCustomNodeInit(c *container.CustomNodeInit) []map[string]interface{} { + result := []map[string]interface{}{} + if c != nil { + result = append(result, map[string]interface{}{ + "init_script": flattenInitScript(c.InitScript), + }) + } + return result +} + +func flattenInitScript(c *container.InitScript) []map[string]interface{} { + result := []map[string]interface{}{} + if c != nil { + result = append(result, map[string]interface{}{ + "gcs_uri": c.GcsUri, + "gcs_generation": c.GcsGeneration, + "gcp_secret_manager_secret_uri": c.GcpSecretManagerSecretUri, + }) + } + return result +} + func flattenSwapConfig(c *container.SwapConfig) []map[string]interface{} { result := []map[string]interface{}{} if c != nil { diff --git a/mmv1/third_party/terraform/services/container/resource_container_cluster_meta.yaml.tmpl b/mmv1/third_party/terraform/services/container/resource_container_cluster_meta.yaml.tmpl index c1b9589cffd4..131567ed1704 100644 --- a/mmv1/third_party/terraform/services/container/resource_container_cluster_meta.yaml.tmpl +++ b/mmv1/third_party/terraform/services/container/resource_container_cluster_meta.yaml.tmpl @@ -384,6 +384,9 @@ fields: - api_field: 'nodeConfig.labels' - api_field: 'nodeConfig.linuxNodeConfig.accurateTimeConfig.enablePtpKvmTimeSync' - api_field: 'nodeConfig.linuxNodeConfig.cgroupMode' + - api_field: 'nodeConfig.linuxNodeConfig.customNodeInit.initScript.gcpSecretManagerSecretUri' + - api_field: 'nodeConfig.linuxNodeConfig.customNodeInit.initScript.gcsGeneration' + - api_field: 'nodeConfig.linuxNodeConfig.customNodeInit.initScript.gcsUri' - field: 'node_config.linux_node_config.hugepages_config.hugepage_size_1g' api_field: 'nodeConfig.linuxNodeConfig.hugepages.hugepageSize1g' - field: 'node_config.linux_node_config.hugepages_config.hugepage_size_2m' @@ -680,6 +683,12 @@ fields: api_field: 'nodePools.config.linuxNodeConfig.accurateTimeConfig.enablePtpKvmTimeSync' - field: 'node_pool.node_config.linux_node_config.cgroup_mode' api_field: 'nodePools.config.linuxNodeConfig.cgroupMode' + - field: 'node_pool.node_config.linux_node_config.custom_node_init.init_script.gcp_secret_manager_secret_uri' + api_field: 'nodePools.config.linuxNodeConfig.customNodeInit.initScript.gcpSecretManagerSecretUri' + - field: 'node_pool.node_config.linux_node_config.custom_node_init.init_script.gcs_generation' + api_field: 'nodePools.config.linuxNodeConfig.customNodeInit.initScript.gcsGeneration' + - field: 'node_pool.node_config.linux_node_config.custom_node_init.init_script.gcs_uri' + api_field: 'nodePools.config.linuxNodeConfig.customNodeInit.initScript.gcsUri' - field: 'node_pool.node_config.linux_node_config.hugepages_config.hugepage_size_1g' api_field: 'nodePools.config.linuxNodeConfig.hugepages.hugepageSize1g' - field: 'node_pool.node_config.linux_node_config.hugepages_config.hugepage_size_2m' diff --git a/mmv1/third_party/terraform/services/container/resource_container_cluster_test.go.tmpl b/mmv1/third_party/terraform/services/container/resource_container_cluster_test.go.tmpl index 6433b5cfa880..0543a3eba0da 100644 --- a/mmv1/third_party/terraform/services/container/resource_container_cluster_test.go.tmpl +++ b/mmv1/third_party/terraform/services/container/resource_container_cluster_test.go.tmpl @@ -18712,6 +18712,103 @@ func TestAccContainerCluster_withNodeCreationConfig(t *testing.T) { }) } +func TestAccContainerCluster_withCustomNodeInitGcs(t *testing.T) { + t.Parallel() + + cluster := fmt.Sprintf("tf-test-cluster-%s", acctest.RandString(t, 10)) + bucketName := fmt.Sprintf("tf-test-bucket-%s", acctest.RandString(t, 10)) + networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") + subnetworkName := tpgcompute.BootstrapSubnet(t, "gke-cluster", networkName) + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccCheckContainerClusterDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccContainerCluster_withCustomNodeInitGcs(cluster, bucketName, networkName, subnetworkName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_container_cluster.primary", "node_config.0.linux_node_config.0.custom_node_init.0.init_script.0.gcs_uri", fmt.Sprintf("gs://%s/script.sh", bucketName)), + resource.TestCheckResourceAttrSet("google_container_cluster.primary", "node_config.0.linux_node_config.0.custom_node_init.0.init_script.0.gcs_generation"), + ), + }, + { + ResourceName: "google_container_cluster.primary", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection", "min_master_version"}, + }, + }, + }) +} + +func TestAccContainerCluster_withCustomNodeInitSecret(t *testing.T) { + t.Parallel() + + cluster := fmt.Sprintf("tf-test-cluster-%s", acctest.RandString(t, 10)) + secretId := fmt.Sprintf("tf-test-secret-%s", acctest.RandString(t, 10)) + networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") + subnetworkName := tpgcompute.BootstrapSubnet(t, "gke-cluster", networkName) + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + ExternalProviders: map[string]resource.ExternalProvider{ + "time": {}, + }, + CheckDestroy: testAccCheckContainerClusterDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccContainerCluster_withCustomNodeInitSecret(cluster, secretId, networkName, subnetworkName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("google_container_cluster.primary", "node_config.0.linux_node_config.0.custom_node_init.0.init_script.0.gcp_secret_manager_secret_uri"), + ), + }, + { + ResourceName: "google_container_cluster.primary", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection", "min_master_version"}, + }, + }, + }) +} + +func TestAccContainerCluster_withCustomNodeInitInline(t *testing.T) { + t.Parallel() + + cluster := fmt.Sprintf("tf-test-cluster-%s", acctest.RandString(t, 10)) + bucketName := fmt.Sprintf("tf-test-bucket-%s", acctest.RandString(t, 10)) + secretId := fmt.Sprintf("tf-test-secret-%s", acctest.RandString(t, 10)) + networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") + subnetworkName := tpgcompute.BootstrapSubnet(t, "gke-cluster", networkName) + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + ExternalProviders: map[string]resource.ExternalProvider{ + "time": {}, + }, + CheckDestroy: testAccCheckContainerClusterDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccContainerCluster_withCustomNodeInitInline(cluster, bucketName, secretId, networkName, subnetworkName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_container_cluster.primary", "node_pool.0.node_config.0.linux_node_config.0.custom_node_init.0.init_script.0.gcs_uri", fmt.Sprintf("gs://%s/script.sh", bucketName)), + resource.TestCheckResourceAttrSet("google_container_cluster.primary", "node_pool.0.node_config.0.linux_node_config.0.custom_node_init.0.init_script.0.gcs_generation"), + resource.TestCheckResourceAttrSet("google_container_cluster.primary", "node_pool.1.node_config.0.linux_node_config.0.custom_node_init.0.init_script.0.gcp_secret_manager_secret_uri"), + ), + }, + { + ResourceName: "google_container_cluster.primary", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection", "min_master_version"}, + }, + }, + }) +} + func testAccContainerCluster_withNodeCreationConfig(name, networkName, subnetworkName string, mode string) string { return fmt.Sprintf(` resource "google_container_cluster" "primary" { @@ -18728,6 +18825,211 @@ resource "google_container_cluster" "primary" { `, name, networkName, subnetworkName, mode) } +func testAccContainerCluster_withCustomNodeInitGcs(cluster, bucket, networkName, subnetworkName string) string { + return fmt.Sprintf(` +data "google_container_engine_versions" "central1a" { + location = "us-central1-a" +} + +resource "google_storage_bucket" "bucket" { + name = "%s" + location = "US" + force_destroy = true +} + +resource "google_storage_bucket_object" "script" { + name = "script.sh" + bucket = google_storage_bucket.bucket.name + content = "#!/bin/bash\necho 'Hello World' > /tmp/hello.txt" +} + +resource "google_container_cluster" "primary" { + name = "%s" + location = "us-central1-a" + initial_node_count = 1 + min_master_version = data.google_container_engine_versions.central1a.latest_master_version + deletion_protection = false + network = "%s" + subnetwork = "%s" + + node_config { + image_type = "COS_CONTAINERD" + oauth_scopes = [ + "https://www.googleapis.com/auth/cloud-platform", + ] + linux_node_config { + custom_node_init { + init_script { + gcs_uri = "gs://${google_storage_bucket.bucket.name}/${google_storage_bucket_object.script.name}" + gcs_generation = google_storage_bucket_object.script.generation + } + } + } + } +} +`, bucket, cluster, networkName, subnetworkName) +} + +func testAccContainerCluster_withCustomNodeInitSecret(cluster, secretId, networkName, subnetworkName string) string { + return fmt.Sprintf(` +data "google_container_engine_versions" "central1a" { + location = "us-central1-a" +} + +data "google_project" "project" {} + +resource "google_secret_manager_secret" "secret" { + secret_id = "%s" + replication { + auto {} + } +} + +resource "google_secret_manager_secret_version" "version" { + secret = google_secret_manager_secret.secret.id + secret_data = "#!/bin/bash\necho 'Hello World' > /tmp/hello.txt" +} + +resource "google_secret_manager_secret_iam_member" "secret_accessor" { + secret_id = google_secret_manager_secret.secret.id + role = "roles/secretmanager.secretAccessor" + member = "serviceAccount:${data.google_project.project.number}-compute@developer.gserviceaccount.com" +} + +resource "time_sleep" "wait_60_seconds" { + create_duration = "60s" + depends_on = [ + google_secret_manager_secret_iam_member.secret_accessor + ] +} + +resource "google_container_cluster" "primary" { + name = "%s" + location = "us-central1-a" + initial_node_count = 1 + min_master_version = data.google_container_engine_versions.central1a.latest_master_version + deletion_protection = false + network = "%s" + subnetwork = "%s" + + node_config { + image_type = "COS_CONTAINERD" + oauth_scopes = [ + "https://www.googleapis.com/auth/cloud-platform", + ] + linux_node_config { + custom_node_init { + init_script { + gcp_secret_manager_secret_uri = google_secret_manager_secret_version.version.name + } + } + } + } + + depends_on = [ + time_sleep.wait_60_seconds + ] +} +`, secretId, cluster, networkName, subnetworkName) +} + +func testAccContainerCluster_withCustomNodeInitInline(cluster, bucket, secretId, networkName, subnetworkName string) string { + return fmt.Sprintf(` +data "google_container_engine_versions" "central1a" { + location = "us-central1-a" +} + +data "google_project" "project" {} + +resource "google_storage_bucket" "bucket" { + name = "%s" + location = "US" + force_destroy = true +} + +resource "google_storage_bucket_object" "script" { + name = "script.sh" + bucket = google_storage_bucket.bucket.name + content = "#!/bin/bash\necho 'Hello World' > /tmp/hello.txt" +} + +resource "google_secret_manager_secret" "secret" { + secret_id = "%s" + replication { + auto {} + } +} + +resource "google_secret_manager_secret_version" "version" { + secret = google_secret_manager_secret.secret.id + secret_data = "#!/bin/bash\necho 'Hello World' > /tmp/hello.txt" +} + +resource "google_secret_manager_secret_iam_member" "secret_accessor" { + secret_id = google_secret_manager_secret.secret.id + role = "roles/secretmanager.secretAccessor" + member = "serviceAccount:${data.google_project.project.number}-compute@developer.gserviceaccount.com" +} + +resource "time_sleep" "wait_60_seconds" { + create_duration = "60s" + depends_on = [ + google_secret_manager_secret_iam_member.secret_accessor + ] +} + +resource "google_container_cluster" "primary" { + name = "%s" + location = "us-central1-a" + min_master_version = data.google_container_engine_versions.central1a.latest_master_version + deletion_protection = false + network = "%s" + subnetwork = "%s" + + node_pool { + name = "gcs-pool" + initial_node_count = 1 + node_config { + image_type = "COS_CONTAINERD" + oauth_scopes = [ + "https://www.googleapis.com/auth/cloud-platform", + ] + linux_node_config { + custom_node_init { + init_script { + gcs_uri = "gs://${google_storage_bucket.bucket.name}/${google_storage_bucket_object.script.name}" + gcs_generation = google_storage_bucket_object.script.generation + } + } + } + } + } + + node_pool { + name = "secret-pool" + initial_node_count = 1 + node_config { + image_type = "COS_CONTAINERD" + oauth_scopes = [ + "https://www.googleapis.com/auth/cloud-platform", + ] + linux_node_config { + custom_node_init { + init_script { + gcp_secret_manager_secret_uri = google_secret_manager_secret_version.version.name + } + } + } + } + } + + depends_on = [ + time_sleep.wait_60_seconds + ] +} +`, bucket, secretId, cluster, networkName, subnetworkName) +} + func TestAccContainerCluster_withSlurmOperatorConfig(t *testing.T) { t.Parallel() diff --git a/mmv1/third_party/terraform/services/container/resource_container_node_pool_meta.yaml.tmpl b/mmv1/third_party/terraform/services/container/resource_container_node_pool_meta.yaml.tmpl index d3022818ce32..6579594519fa 100644 --- a/mmv1/third_party/terraform/services/container/resource_container_node_pool_meta.yaml.tmpl +++ b/mmv1/third_party/terraform/services/container/resource_container_node_pool_meta.yaml.tmpl @@ -220,6 +220,12 @@ fields: api_field: 'config.linuxNodeConfig.accurateTimeConfig.enablePtpKvmTimeSync' - field: 'node_config.linux_node_config.cgroup_mode' api_field: 'config.linuxNodeConfig.cgroupMode' + - field: 'node_config.linux_node_config.custom_node_init.init_script.gcp_secret_manager_secret_uri' + api_field: 'config.linuxNodeConfig.customNodeInit.initScript.gcpSecretManagerSecretUri' + - field: 'node_config.linux_node_config.custom_node_init.init_script.gcs_generation' + api_field: 'config.linuxNodeConfig.customNodeInit.initScript.gcsGeneration' + - field: 'node_config.linux_node_config.custom_node_init.init_script.gcs_uri' + api_field: 'config.linuxNodeConfig.customNodeInit.initScript.gcsUri' - field: 'node_config.linux_node_config.hugepages_config.hugepage_size_1g' api_field: 'config.linuxNodeConfig.hugepages.hugepageSize1g' - field: 'node_config.linux_node_config.hugepages_config.hugepage_size_2m' diff --git a/mmv1/third_party/terraform/services/container/resource_container_node_pool_test.go.tmpl b/mmv1/third_party/terraform/services/container/resource_container_node_pool_test.go.tmpl index 008679561fd8..c0a923860c82 100644 --- a/mmv1/third_party/terraform/services/container/resource_container_node_pool_test.go.tmpl +++ b/mmv1/third_party/terraform/services/container/resource_container_node_pool_test.go.tmpl @@ -7432,3 +7432,187 @@ resource "google_container_node_pool" "np" { } `, cluster, networkName, subnetworkName, np, behavior) } + +func TestAccContainerNodePool_withCustomNodeInit(t *testing.T) { + t.Parallel() + + cluster := fmt.Sprintf("tf-test-cluster-%s", acctest.RandString(t, 10)) + nodePool := fmt.Sprintf("tf-test-nodepool-%s", acctest.RandString(t, 10)) + bucketName := fmt.Sprintf("tf-test-bucket-%s", acctest.RandString(t, 10)) + networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") + subnetworkName := tpgcompute.BootstrapSubnet(t, "gke-cluster", networkName) + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccCheckContainerNodePoolDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccContainerNodePool_withCustomNodeInit(cluster, nodePool, bucketName, networkName, subnetworkName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_container_node_pool.np_with_custom_node_init", "node_config.0.linux_node_config.0.custom_node_init.0.init_script.0.gcs_uri", fmt.Sprintf("gs://%s/script.sh", bucketName)), + resource.TestCheckResourceAttrSet("google_container_node_pool.np_with_custom_node_init", "node_config.0.linux_node_config.0.custom_node_init.0.init_script.0.gcs_generation"), + ), + }, + { + ResourceName: "google_container_node_pool.np_with_custom_node_init", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"initial_node_count", "cluster"}, + }, + }, + }) +} + +func testAccContainerNodePool_withCustomNodeInit(cluster, np, bucket, networkName, subnetworkName string) string { + return fmt.Sprintf(` +data "google_container_engine_versions" "central1a" { + location = "us-central1-a" +} + +resource "google_storage_bucket" "bucket" { + name = "%s" + location = "US" + force_destroy = true +} + +resource "google_storage_bucket_object" "script" { + name = "script.sh" + bucket = google_storage_bucket.bucket.name + content = "#!/bin/bash\necho 'Hello World' > /tmp/hello.txt" +} + +resource "google_container_cluster" "cluster" { + name = "%s" + location = "us-central1-a" + initial_node_count = 1 + min_master_version = data.google_container_engine_versions.central1a.latest_master_version + deletion_protection = false + network = "%s" + subnetwork = "%s" +} + +resource "google_container_node_pool" "np_with_custom_node_init" { + name = "%s" + location = "us-central1-a" + cluster = google_container_cluster.cluster.name + initial_node_count = 1 + node_config { + image_type = "COS_CONTAINERD" + oauth_scopes = [ + "https://www.googleapis.com/auth/cloud-platform", + ] + linux_node_config { + custom_node_init { + init_script { + gcs_uri = "gs://${google_storage_bucket.bucket.name}/${google_storage_bucket_object.script.name}" + gcs_generation = google_storage_bucket_object.script.generation + } + } + } + } +} +`, bucket, cluster, networkName, subnetworkName, np) +} + +func TestAccContainerNodePool_withCustomNodeInitSecret(t *testing.T) { + t.Parallel() + + cluster := fmt.Sprintf("tf-test-cluster-%s", acctest.RandString(t, 10)) + nodePool := fmt.Sprintf("tf-test-nodepool-%s", acctest.RandString(t, 10)) + secretId := fmt.Sprintf("tf-test-secret-%s", acctest.RandString(t, 10)) + networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") + subnetworkName := tpgcompute.BootstrapSubnet(t, "gke-cluster", networkName) + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + ExternalProviders: map[string]resource.ExternalProvider{ + "time": {}, + }, + CheckDestroy: testAccCheckContainerNodePoolDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccContainerNodePool_withCustomNodeInitSecret(cluster, nodePool, secretId, networkName, subnetworkName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("google_container_node_pool.np_with_custom_node_init_secret", "node_config.0.linux_node_config.0.custom_node_init.0.init_script.0.gcp_secret_manager_secret_uri"), + ), + }, + { + ResourceName: "google_container_node_pool.np_with_custom_node_init_secret", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"initial_node_count", "cluster"}, + }, + }, + }) +} + +func testAccContainerNodePool_withCustomNodeInitSecret(cluster, np, secretId, networkName, subnetworkName string) string { + return fmt.Sprintf(` +data "google_container_engine_versions" "central1a" { + location = "us-central1-a" +} + +data "google_project" "project" {} + +resource "google_secret_manager_secret" "secret" { + secret_id = "%s" + replication { + auto {} + } +} + +resource "google_secret_manager_secret_version" "version" { + secret = google_secret_manager_secret.secret.id + secret_data = "#!/bin/bash\necho 'Hello World' > /tmp/hello.txt" +} + +resource "google_secret_manager_secret_iam_member" "secret_accessor" { + secret_id = google_secret_manager_secret.secret.id + role = "roles/secretmanager.secretAccessor" + member = "serviceAccount:${data.google_project.project.number}-compute@developer.gserviceaccount.com" +} + +resource "google_container_cluster" "cluster" { + name = "%s" + location = "us-central1-a" + initial_node_count = 1 + min_master_version = data.google_container_engine_versions.central1a.latest_master_version + deletion_protection = false + network = "%s" + subnetwork = "%s" +} + +resource "time_sleep" "wait_60_seconds" { + create_duration = "60s" + depends_on = [ + google_secret_manager_secret_iam_member.secret_accessor + ] +} + +resource "google_container_node_pool" "np_with_custom_node_init_secret" { + name = "%s" + location = "us-central1-a" + cluster = google_container_cluster.cluster.name + initial_node_count = 1 + node_config { + image_type = "COS_CONTAINERD" + oauth_scopes = [ + "https://www.googleapis.com/auth/cloud-platform", + ] + linux_node_config { + custom_node_init { + init_script { + gcp_secret_manager_secret_uri = google_secret_manager_secret_version.version.name + } + } + } + } + + depends_on = [ + time_sleep.wait_60_seconds + ] +} +`, secretId, cluster, networkName, subnetworkName, np) +} diff --git a/mmv1/third_party/terraform/website/docs/r/container_cluster.html.markdown b/mmv1/third_party/terraform/website/docs/r/container_cluster.html.markdown index 4eb233d3d340..6c260a6253fb 100644 --- a/mmv1/third_party/terraform/website/docs/r/container_cluster.html.markdown +++ b/mmv1/third_party/terraform/website/docs/r/container_cluster.html.markdown @@ -1789,6 +1789,8 @@ linux_node_config { * `accurate_time_config` - (Optional) Accurate time configuration for the node. Structure is [documented below](#nested_accurate_time_config). +* `custom_node_init` - (Optional) Custom node init settings. Structure is [documented below](#nested_custom_node_init). + The `swap_config` block supports: * `enabled` - (Optional) Enables or disables swap for the node pool. @@ -1855,6 +1857,18 @@ linux_node_config { * `ENFORCE_SIGNED_MODULES`: Enforced signature verification: Node pools will use a Container-Optimized OS image configured to allow loading of *Google-signed* external kernel modules. Loadpin is enabled but configured to exclude modules, and kernel module signature checking is enforced. * `DO_NOT_ENFORCE_SIGNED_MODULES`: Mirrors existing DEFAULT behavior: For CPU and TPU nodes, the image will not allow loading external kernel modules. For GPU nodes, the image will allow loading any module, whether it is signed or not. +The `custom_node_init` block supports: + +* `init_script` - (Optional) The init script configuration. Structure is [documented below](#nested_init_script). + +The `init_script` block supports: + +* `gcs_uri` - (Optional) The Google Cloud Storage URI for storing the init script. Format: `gs://BUCKET_NAME/OBJECT_NAME`. The service account on the nodepool must have read access to the object. Conflicts with `gcp_secret_manager_secret_uri`. If `gcs_uri` is used, `gcs_generation` is required. + +* `gcs_generation` - (Optional) The generation of the init script in Google Cloud Storage. If `gcs_uri` is used, `gcs_generation` is required. + +* `gcp_secret_manager_secret_uri` - (Optional) The Google Cloud Secret Manager secret version URI for storing the init script. Format: `projects/PROJECT_ID/secrets/SECRET_NAME/versions/VERSION`. The service account on the nodepool must have access to the secret version. Conflicts with `gcs_uri`. + The `containerd_config` block supports: * `private_registry_access_config` (Optional) - Configuration for private container registries. There are two fields in this config: diff --git a/mmv1/third_party/tgc_next/pkg/services/container/node_config.go b/mmv1/third_party/tgc_next/pkg/services/container/node_config.go index 49ffc0dac3de..c82bd7c39932 100644 --- a/mmv1/third_party/tgc_next/pkg/services/container/node_config.go +++ b/mmv1/third_party/tgc_next/pkg/services/container/node_config.go @@ -1160,6 +1160,42 @@ func schemaNodeConfig() *schema.Schema { }, }, }, + "custom_node_init": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Description: `The custom node init settings.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "init_script": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Description: `The init script configuration.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "gcs_uri": { + Type: schema.TypeString, + Optional: true, + Description: `The GCS URI of the init script.`, + }, + "gcs_generation": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + Description: `The GCS generation of the init script.`, + }, + "gcp_secret_manager_secret_uri": { + Type: schema.TypeString, + Optional: true, + Description: `The Secret Manager secret URI of the init script.`, + }, + }, + }, + }, + }, + }, + }, }, }, }, @@ -2164,6 +2200,10 @@ func expandLinuxNodeConfig(v interface{}) *container.LinuxNodeConfig { linuxNodeConfig.SwapConfig = expandSwapConfig(v) } + if v, ok := cfg["custom_node_init"]; ok { + linuxNodeConfig.CustomNodeInit = expandCustomNodeInit(v) + } + return linuxNodeConfig } @@ -3347,6 +3387,7 @@ func flattenLinuxNodeConfig(v interface{}) []map[string]interface{} { "node_kernel_module_loading": flattenNodeKernelModuleLoading(c["nodeKernelModuleLoading"]), "swap_config": flattenSwapConfig(c["swapConfig"]), "accurate_time_config": flattenAccurateTimeConfig(c["accurateTimeConfig"]), + "custom_node_init": flattenCustomNodeInit(c["customNodeInit"]), } return []map[string]interface{}{transformed} @@ -3797,3 +3838,93 @@ func flattenGpuDirectConfig(v interface{}) string { } return strategy } + +func expandCustomNodeInit(v interface{}) *container.CustomNodeInit { + if v == nil { + return nil + } + ls := v.([]interface{}) + if len(ls) == 0 { + return nil + } + if ls[0] == nil { + return &container.CustomNodeInit{} + } + cfg := ls[0].(map[string]interface{}) + + customNodeInit := &container.CustomNodeInit{} + if v, ok := cfg["init_script"]; ok { + customNodeInit.InitScript = expandInitScript(v) + } + return customNodeInit +} + +func expandInitScript(v interface{}) *container.InitScript { + if v == nil { + return nil + } + ls := v.([]interface{}) + if len(ls) == 0 { + return nil + } + if ls[0] == nil { + return &container.InitScript{} + } + cfg := ls[0].(map[string]interface{}) + + initScript := &container.InitScript{} + if v, ok := cfg["gcs_uri"]; ok { + initScript.GcsUri = v.(string) + } + if v, ok := cfg["gcs_generation"]; ok { + initScript.GcsGeneration = int64(v.(int)) + } + if v, ok := cfg["gcp_secret_manager_secret_uri"]; ok { + initScript.GcpSecretManagerSecretUri = v.(string) + } + return initScript +} + +func flattenCustomNodeInit(v interface{}) []map[string]interface{} { + if v == nil { + return nil + } + c, ok := v.(map[string]interface{}) + if !ok { + return nil + } + transformed := map[string]interface{}{} + if is, ok := c["initScript"].(map[string]interface{}); ok { + transformed["init_script"] = flattenInitScript(is) + } + return []map[string]interface{}{transformed} +} + +func flattenInitScript(v interface{}) []map[string]interface{} { + if v == nil { + return nil + } + c, ok := v.(map[string]interface{}) + if !ok { + return nil + } + transformed := map[string]interface{}{} + if val, ok := c["gcsUri"]; ok { + transformed["gcs_uri"] = val + } + if val, ok := c["gcsGeneration"]; ok { + var gen interface{} = val + if strGen, ok := val.(string); ok { + if intGen, err := strconv.Atoi(strGen); err == nil { + gen = intGen + } + } else if floatGen, ok := val.(float64); ok { + gen = int(floatGen) + } + transformed["gcs_generation"] = gen + } + if val, ok := c["gcpSecretManagerSecretUri"]; ok { + transformed["gcp_secret_manager_secret_uri"] = val + } + return []map[string]interface{}{transformed} +} diff --git a/mmv1/third_party/tgc_next/pkg/services/container/resource_container_cluster_tfplan2cai.go b/mmv1/third_party/tgc_next/pkg/services/container/resource_container_cluster_tfplan2cai.go index 0b3dbbae3966..c26c956aa2ca 100644 --- a/mmv1/third_party/tgc_next/pkg/services/container/resource_container_cluster_tfplan2cai.go +++ b/mmv1/third_party/tgc_next/pkg/services/container/resource_container_cluster_tfplan2cai.go @@ -2,6 +2,7 @@ package container import ( "fmt" + "log" "strings" "time" @@ -643,11 +644,17 @@ func expandMaintenancePolicy(d tpgresource.TerraformResourceData, config *transp if err != nil { return nil } - clusterGetCall := NewClient(config, userAgent).Projects.Locations.Clusters.Get(name) - if config.UserProjectOverride { - clusterGetCall.Header().Add("X-Goog-User-Project", project) + var cluster *container.Cluster + client := NewClient(config, userAgent) + if client != nil { + clusterGetCall := client.Projects.Locations.Clusters.Get(name) + if config.UserProjectOverride { + clusterGetCall.Header().Add("X-Goog-User-Project", project) + } + cluster, _ = clusterGetCall.Do() + } else { + log.Printf("[WARN] GKE client is nil, skipping cluster GET call") } - cluster, _ := clusterGetCall.Do() resourceVersion := "" exclusions := make(map[string]container.TimeWindow) if cluster != nil && cluster.MaintenancePolicy != nil { diff --git a/mmv1/third_party/tgc_next/pkg/tfplan2cai/convert_test.go b/mmv1/third_party/tgc_next/pkg/tfplan2cai/convert_test.go index 778d1fb41649..5a4e360c1bae 100644 --- a/mmv1/third_party/tgc_next/pkg/tfplan2cai/convert_test.go +++ b/mmv1/third_party/tgc_next/pkg/tfplan2cai/convert_test.go @@ -213,3 +213,49 @@ func TestConvert_ComputeAddress(t *testing.T) { assert.NotEmpty(t, assets) assert.Equal(t, "https://www.googleapis.com/compute/v1/projects/terraform-dev-haonan/regions/us-east1/subnetworks/subnetwork-test", assets[1].Resource.Data["subnetwork"]) } + +func TestConvert_GkeCustomNodeInit(t *testing.T) { + logger, _ := newTestErrorLogger() + o := &Options{ + ErrorLogger: logger, + Offline: true, + DefaultProject: testProject, + DefaultZone: "us-central1-a", + NoOpAncestryManager: true, + } + + jsonPlan, err := os.ReadFile("resolvers/gke_custom_node_init.tfplan.json") + if err != nil { + t.Fatalf("Error reading test file: %v", err) + } + + assets, err := Convert(context.Background(), jsonPlan, o) + if err != nil { + t.Fatalf("Error marshaling assets: %v", err) + } + assert.Nil(t, err) + assert.NotEmpty(t, assets) + + var clusterAsset *caiasset.Asset + for _, asset := range assets { + if asset.Type == "container.googleapis.com/Cluster" { + clusterAsset = &asset + break + } + } + assert.NotNil(t, clusterAsset, "Cluster asset not found") + + nodeConfig, ok := clusterAsset.Resource.Data["nodeConfig"].(map[string]interface{}) + assert.True(t, ok, "nodeConfig should be a map") + + linuxNodeConfig, ok := nodeConfig["linuxNodeConfig"].(map[string]interface{}) + assert.True(t, ok, "linuxNodeConfig should be a map") + + customNodeInit, ok := linuxNodeConfig["customNodeInit"].(map[string]interface{}) + assert.True(t, ok, "customNodeInit should be a map") + + initScript, ok := customNodeInit["initScript"].(map[string]interface{}) + assert.True(t, ok, "initScript should be a map") + + assert.Equal(t, "gs://my-bucket/init.sh", initScript["gcsUri"]) +} diff --git a/mmv1/third_party/tgc_next/pkg/tfplan2cai/resolvers/gke_custom_node_init.tfplan.json b/mmv1/third_party/tgc_next/pkg/tfplan2cai/resolvers/gke_custom_node_init.tfplan.json new file mode 100644 index 000000000000..985b9af88e24 --- /dev/null +++ b/mmv1/third_party/tgc_next/pkg/tfplan2cai/resolvers/gke_custom_node_init.tfplan.json @@ -0,0 +1 @@ +{"format_version":"1.2","terraform_version":"1.9.0","planned_values":{"root_module":{"resources":[{"address":"google_container_cluster.primary","mode":"managed","type":"google_container_cluster","name":"primary","provider_name":"registry.terraform.io/hashicorp/google-beta","schema_version":2,"values":{"allow_net_admin":null,"binary_authorization":[],"deletion_policy":"DELETE","deletion_protection":false,"description":null,"disable_l4_lb_firewall_reconciliation":false,"dns_config":[],"effective_labels":{"goog-terraform-provisioned":"true"},"enable_autopilot":null,"enable_cilium_clusterwide_network_policy":false,"enable_fqdn_network_policy":false,"enable_k8s_beta_apis":[],"enable_kubernetes_alpha":false,"enable_legacy_abac":false,"enable_multi_networking":false,"enable_shielded_nodes":true,"fleet":[],"in_transit_encryption_config":null,"initial_node_count":1,"location":"us-central1-a","maintenance_policy":[],"min_master_version":null,"name":"marss-cluster","network":"default","network_performance_config":[],"network_policy":[],"node_config":[{"advanced_machine_features":[],"boot_disk_kms_key":null,"enable_confidential_storage":null,"ephemeral_storage_config":[],"ephemeral_storage_local_ssd_config":[],"fast_socket":[],"flex_start":null,"gvnic":[],"host_maintenance_policy":[],"linux_node_config":[{"accurate_time_config":[],"custom_node_init":[{"init_script":[{"gcp_secret_manager_secret_uri":null,"gcs_uri":"gs://my-bucket/init.sh"}]}],"hugepages_config":[],"node_kernel_module_loading":[],"swap_config":[],"sysctls":null,"transparent_hugepage_defrag":null}],"local_nvme_ssd_block_config":[],"local_ssd_encryption_mode":null,"max_run_duration":null,"node_group":null,"preemptible":false,"reservation_affinity":[],"resource_labels":null,"resource_manager_tags":null,"sandbox_config":[],"secondary_boot_disks":[],"sole_tenant_config":[],"spot":false,"storage_pools":null,"tags":null,"taint":[]}],"pod_security_policy_config":[],"private_cluster_config":[],"remove_default_node_pool":null,"resource_labels":null,"resource_usage_export_config":[],"secret_manager_config":[],"secret_sync_config":[],"terraform_labels":{"goog-terraform-provisioned":"true"},"timeouts":null,"user_managed_keys_config":[]},"sensitive_values":{"addons_config":[],"anonymous_authentication_config":[],"authenticator_groups_config":[],"autopilot_cluster_policy_config":[],"autopilot_privileged_admission":[],"binary_authorization":[],"cluster_autoscaling":[],"cluster_telemetry":[],"confidential_nodes":[],"control_plane_endpoints_config":[],"cost_management_config":[],"database_encryption":[],"default_snat_status":[],"dns_config":[],"effective_labels":{},"enable_k8s_beta_apis":[],"enterprise_config":[],"fleet":[],"gateway_api_config":[],"gke_auto_upgrade_config":[],"identity_service_config":[],"ip_allocation_policy":[],"logging_config":[],"maintenance_policy":[],"managed_machine_learning_diagnostics_config":[],"managed_opentelemetry_config":[],"master_auth":[],"master_authorized_networks_config":[],"mesh_certificates":[],"monitoring_config":[],"network_performance_config":[],"network_policy":[],"node_config":[{"advanced_machine_features":[],"boot_disk":[],"confidential_nodes":[],"containerd_config":[],"effective_taints":[],"ephemeral_storage_config":[],"ephemeral_storage_local_ssd_config":[],"fast_socket":[],"gcfs_config":[],"guest_accelerator":[],"gvnic":[],"host_maintenance_policy":[],"kubelet_config":[],"labels":{},"linux_node_config":[{"accurate_time_config":[],"custom_node_init":[{"init_script":[{}]}],"hugepages_config":[],"node_kernel_module_loading":[],"swap_config":[]}],"local_nvme_ssd_block_config":[],"metadata":{},"oauth_scopes":[],"reservation_affinity":[],"sandbox_config":[],"secondary_boot_disks":[],"shielded_instance_config":[],"sole_tenant_config":[],"taint":[],"windows_node_config":[],"workload_metadata_config":[]}],"node_locations":[],"node_pool":[],"node_pool_auto_config":[],"node_pool_defaults":[],"notification_config":[],"pod_autoscaling":[],"pod_security_policy_config":[],"private_cluster_config":[],"protect_config":[],"rbac_binding_config":[],"release_channel":[],"resource_usage_export_config":[],"secret_manager_config":[],"secret_sync_config":[],"security_posture_config":[],"service_external_ips_config":[],"terraform_labels":{},"tpu_config":[],"user_managed_keys_config":[],"vertical_pod_autoscaling":[],"workload_alts_config":[],"workload_identity_config":[]}}]}},"resource_changes":[{"address":"google_container_cluster.primary","mode":"managed","type":"google_container_cluster","name":"primary","provider_name":"registry.terraform.io/hashicorp/google-beta","change":{"actions":["create"],"before":null,"after":{"allow_net_admin":null,"binary_authorization":[],"deletion_policy":"DELETE","deletion_protection":false,"description":null,"disable_l4_lb_firewall_reconciliation":false,"dns_config":[],"effective_labels":{"goog-terraform-provisioned":"true"},"enable_autopilot":null,"enable_cilium_clusterwide_network_policy":false,"enable_fqdn_network_policy":false,"enable_k8s_beta_apis":[],"enable_kubernetes_alpha":false,"enable_legacy_abac":false,"enable_multi_networking":false,"enable_shielded_nodes":true,"fleet":[],"in_transit_encryption_config":null,"initial_node_count":1,"location":"us-central1-a","maintenance_policy":[],"min_master_version":null,"name":"marss-cluster","network":"default","network_performance_config":[],"network_policy":[],"node_config":[{"advanced_machine_features":[],"boot_disk_kms_key":null,"enable_confidential_storage":null,"ephemeral_storage_config":[],"ephemeral_storage_local_ssd_config":[],"fast_socket":[],"flex_start":null,"gvnic":[],"host_maintenance_policy":[],"linux_node_config":[{"accurate_time_config":[],"custom_node_init":[{"init_script":[{"gcp_secret_manager_secret_uri":null,"gcs_uri":"gs://my-bucket/init.sh"}]}],"hugepages_config":[],"node_kernel_module_loading":[],"swap_config":[],"sysctls":null,"transparent_hugepage_defrag":null}],"local_nvme_ssd_block_config":[],"local_ssd_encryption_mode":null,"max_run_duration":null,"node_group":null,"preemptible":false,"reservation_affinity":[],"resource_labels":null,"resource_manager_tags":null,"sandbox_config":[],"secondary_boot_disks":[],"sole_tenant_config":[],"spot":false,"storage_pools":null,"tags":null,"taint":[]}],"pod_security_policy_config":[],"private_cluster_config":[],"remove_default_node_pool":null,"resource_labels":null,"resource_usage_export_config":[],"secret_manager_config":[],"secret_sync_config":[],"terraform_labels":{"goog-terraform-provisioned":"true"},"timeouts":null,"user_managed_keys_config":[]},"after_unknown":{"addons_config":true,"anonymous_authentication_config":true,"authenticator_groups_config":true,"autopilot_cluster_policy_config":true,"autopilot_privileged_admission":true,"binary_authorization":[],"cluster_autoscaling":true,"cluster_ipv4_cidr":true,"cluster_telemetry":true,"confidential_nodes":true,"control_plane_endpoints_config":true,"cost_management_config":true,"database_encryption":true,"datapath_provider":true,"default_max_pods_per_node":true,"default_snat_status":true,"dns_config":[],"effective_labels":{},"enable_intranode_visibility":true,"enable_k8s_beta_apis":[],"enable_l4_ilb_subsetting":true,"enable_tpu":true,"endpoint":true,"enterprise_config":true,"fleet":[],"gateway_api_config":true,"gke_auto_upgrade_config":true,"id":true,"identity_service_config":true,"ip_allocation_policy":true,"label_fingerprint":true,"logging_config":true,"logging_service":true,"maintenance_policy":[],"managed_machine_learning_diagnostics_config":true,"managed_opentelemetry_config":true,"master_auth":true,"master_authorized_networks_config":true,"master_version":true,"mesh_certificates":true,"monitoring_config":true,"monitoring_service":true,"network_performance_config":[],"network_policy":[],"networking_mode":true,"node_config":[{"advanced_machine_features":[],"boot_disk":true,"confidential_nodes":true,"containerd_config":true,"disk_size_gb":true,"disk_type":true,"effective_taints":true,"ephemeral_storage_config":[],"ephemeral_storage_local_ssd_config":[],"fast_socket":[],"gcfs_config":true,"gpudirect_strategy":true,"guest_accelerator":true,"gvnic":[],"host_maintenance_policy":[],"image_type":true,"kubelet_config":true,"labels":true,"linux_node_config":[{"accurate_time_config":[],"cgroup_mode":true,"custom_node_init":[{"init_script":[{"gcs_generation":true}]}],"hugepages_config":[],"node_kernel_module_loading":[],"swap_config":[],"transparent_hugepage_enabled":true}],"local_nvme_ssd_block_config":[],"local_ssd_count":true,"logging_variant":true,"machine_type":true,"metadata":true,"min_cpu_platform":true,"oauth_scopes":true,"reservation_affinity":[],"sandbox_config":[],"secondary_boot_disks":[],"service_account":true,"shielded_instance_config":true,"sole_tenant_config":[],"taint":[],"windows_node_config":true,"workload_metadata_config":true}],"node_locations":true,"node_pool":true,"node_pool_auto_config":true,"node_pool_defaults":true,"node_version":true,"notification_config":true,"operation":true,"pod_autoscaling":true,"pod_security_policy_config":[],"private_cluster_config":[],"private_ipv6_google_access":true,"project":true,"protect_config":true,"rbac_binding_config":true,"release_channel":true,"resource_usage_export_config":[],"secret_manager_config":[],"secret_sync_config":[],"security_posture_config":true,"self_link":true,"service_external_ips_config":true,"services_ipv4_cidr":true,"subnetwork":true,"terraform_labels":{},"tpu_config":true,"tpu_ipv4_cidr_block":true,"user_managed_keys_config":[],"vertical_pod_autoscaling":true,"workload_alts_config":true,"workload_identity_config":true},"before_sensitive":false,"after_sensitive":{"addons_config":[],"anonymous_authentication_config":[],"authenticator_groups_config":[],"autopilot_cluster_policy_config":[],"autopilot_privileged_admission":[],"binary_authorization":[],"cluster_autoscaling":[],"cluster_telemetry":[],"confidential_nodes":[],"control_plane_endpoints_config":[],"cost_management_config":[],"database_encryption":[],"default_snat_status":[],"dns_config":[],"effective_labels":{},"enable_k8s_beta_apis":[],"enterprise_config":[],"fleet":[],"gateway_api_config":[],"gke_auto_upgrade_config":[],"identity_service_config":[],"ip_allocation_policy":[],"logging_config":[],"maintenance_policy":[],"managed_machine_learning_diagnostics_config":[],"managed_opentelemetry_config":[],"master_auth":[],"master_authorized_networks_config":[],"mesh_certificates":[],"monitoring_config":[],"network_performance_config":[],"network_policy":[],"node_config":[{"advanced_machine_features":[],"boot_disk":[],"confidential_nodes":[],"containerd_config":[],"effective_taints":[],"ephemeral_storage_config":[],"ephemeral_storage_local_ssd_config":[],"fast_socket":[],"gcfs_config":[],"guest_accelerator":[],"gvnic":[],"host_maintenance_policy":[],"kubelet_config":[],"labels":{},"linux_node_config":[{"accurate_time_config":[],"custom_node_init":[{"init_script":[{}]}],"hugepages_config":[],"node_kernel_module_loading":[],"swap_config":[]}],"local_nvme_ssd_block_config":[],"metadata":{},"oauth_scopes":[],"reservation_affinity":[],"sandbox_config":[],"secondary_boot_disks":[],"shielded_instance_config":[],"sole_tenant_config":[],"taint":[],"windows_node_config":[],"workload_metadata_config":[]}],"node_locations":[],"node_pool":[],"node_pool_auto_config":[],"node_pool_defaults":[],"notification_config":[],"pod_autoscaling":[],"pod_security_policy_config":[],"private_cluster_config":[],"protect_config":[],"rbac_binding_config":[],"release_channel":[],"resource_usage_export_config":[],"secret_manager_config":[],"secret_sync_config":[],"security_posture_config":[],"service_external_ips_config":[],"terraform_labels":{},"tpu_config":[],"user_managed_keys_config":[],"vertical_pod_autoscaling":[],"workload_alts_config":[],"workload_identity_config":[]}}}],"configuration":{"provider_config":{"google-beta":{"name":"google-beta","full_name":"registry.terraform.io/hashicorp/google-beta","expressions":{"project":{"constant_value":"zicong-gke-dev"},"region":{"constant_value":"us-central1"},"zone":{"constant_value":"us-central1-a"}}}},"root_module":{"resources":[{"address":"google_container_cluster.primary","mode":"managed","type":"google_container_cluster","name":"primary","provider_config_key":"google-beta","expressions":{"deletion_protection":{"constant_value":false},"initial_node_count":{"constant_value":1},"location":{"constant_value":"us-central1-a"},"name":{"constant_value":"marss-cluster"},"node_config":[{"linux_node_config":[{"custom_node_init":[{"init_script":[{"gcs_uri":{"constant_value":"gs://my-bucket/init.sh"}}]}]}]}]},"schema_version":2}]}},"timestamp":"2026-06-02T20:44:49Z","applyable":true,"complete":true,"errored":false} From abd9ecec313d0384fb1ef1192c5cd4b64552fa61 Mon Sep 17 00:00:00 2001 From: guvenenb Date: Thu, 9 Jul 2026 23:02:33 +0200 Subject: [PATCH 74/78] Update class_methods documentation for Vertex AI Reasoning Engine (#18181) --- mmv1/products/vertexai/ReasoningEngine.yaml | 246 ++++++++++++++++++++ 1 file changed, 246 insertions(+) diff --git a/mmv1/products/vertexai/ReasoningEngine.yaml b/mmv1/products/vertexai/ReasoningEngine.yaml index 4c7d39ce5949..5d8389861418 100644 --- a/mmv1/products/vertexai/ReasoningEngine.yaml +++ b/mmv1/products/vertexai/ReasoningEngine.yaml @@ -251,6 +251,252 @@ properties: description: | Optional. Declarations for object class methods in OpenAPI specification format. + + **Note**: When deploying via Terraform, this field must be populated manually. + Otherwise, client SDKs (like `agent_engines.get()`) will not be able to discover the methods, and calls to the engine (or A2A integrations) will fail. + + Depending on the template/framework used (`agent_framework`), the required class methods and their parameters differ: + + **Warning**: The configuration snippets below are illustrative, may not be exhaustive, and could stop working over time. For the most up-to-date method lists and schemas, please consult the respective SDK source code: + * For Google ADK: See [ADK Python SDK cli_deploy.py](https://github.com/google/adk-python/blob/68a780306e3bdd648a882ef34c0abf8e5148353e/src/google/adk/cli/cli_deploy.py#L109). + * For Langchain: See [Vertex AI Python SDK langchain.py](https://github.com/googleapis/python-aiplatform/blob/c8a38a085931b01f4d6071f0ab7a64cb42851829/agentplatform/agent_engines/templates/langchain.py#L642-L717). + + ### 1. Langchain Template + * `query` (api_mode = "sync" or empty) + * `stream_query` (api_mode = "stream") + + Example for Langchain: + ```hcl + class_methods = jsonencode([ + { + name = "query" + api_mode = "sync" + description = "Queries the reasoning engine" + parameters = { + type = "object" + required = ["input"] + properties = { + input = { + type = "string" + description = "The input prompt" + } + } + } + }, + { + name = "stream_query" + api_mode = "stream" + description = "Streams queries from the reasoning engine" + parameters = { + type = "object" + required = ["input"] + properties = { + input = { + type = "string" + description = "The input prompt" + } + } + } + } + ]) + ``` + + ### 2. Google ADK Template (Standard - No A2A) + For standard Google ADK (Agent Development Kit) deployments, you must define the following 11 methods: + + Example for Standard ADK: + ```hcl + class_methods = jsonencode([ + { + name = "get_session" + api_mode = "" + description = "Retrieve session by ID" + parameters = { + type = "object" + required = ["user_id", "session_id"] + properties = { + user_id = { type = "string" } + session_id = { type = "string" } + } + } + }, + { + name = "async_get_session" + api_mode = "async" + description = "Retrieve session asynchronously by ID" + parameters = { + type = "object" + required = ["user_id", "session_id"] + properties = { + user_id = { type = "string" } + session_id = { type = "string" } + } + } + }, + { + name = "list_sessions" + api_mode = "" + description = "List all sessions for a user" + parameters = { + type = "object" + required = ["user_id"] + properties = { + user_id = { type = "string" } + } + } + }, + { + name = "async_list_sessions" + api_mode = "async" + description = "List all sessions for a user asynchronously" + parameters = { + type = "object" + required = ["user_id"] + properties = { + user_id = { type = "string" } + } + } + }, + { + name = "create_session" + api_mode = "" + description = "Create a new session" + parameters = { + type = "object" + required = ["user_id"] + properties = { + user_id = { type = "string" } + session_id = { type = "string" } + state = { type = "object" } + } + } + }, + { + name = "async_create_session" + api_mode = "async" + description = "Create a new session asynchronously" + parameters = { + type = "object" + required = ["user_id"] + properties = { + user_id = { type = "string" } + session_id = { type = "string" } + state = { type = "object" } + } + } + }, + { + name = "delete_session" + api_mode = "" + description = "Delete session by ID" + parameters = { + type = "object" + required = ["user_id", "session_id"] + properties = { + user_id = { type = "string" } + session_id = { type = "string" } + } + } + }, + { + name = "async_delete_session" + api_mode = "async" + description = "Delete session asynchronously by ID" + parameters = { + type = "object" + required = ["user_id", "session_id"] + properties = { + user_id = { type = "string" } + session_id = { type = "string" } + } + } + }, + { + name = "stream_query" + api_mode = "stream" + description = "Stream queries from the agent" + parameters = { + type = "object" + required = ["message", "user_id"] + properties = { + message = { description = "Message string or object" } + user_id = { type = "string" } + session_id = { type = "string" } + run_config = { type = "object" } + } + } + }, + { + name = "async_stream_query" + api_mode = "async_stream" + description = "Stream queries asynchronously from the agent" + parameters = { + type = "object" + required = ["message", "user_id"] + properties = { + message = { description = "Message string or object" } + user_id = { type = "string" } + session_id = { type = "string" } + session_events = { type = "array", items = { type = "object" } } + run_config = { type = "object" } + } + } + }, + { + name = "streaming_agent_run_with_events" + api_mode = "async_stream" + description = "Stream agent run with events asynchronously" + parameters = { + type = "object" + required = ["request_json"] + properties = { + request_json = { type = "string" } + } + } + } + ]) + ``` + + ### 3. Google ADK Template (A2A-Enabled) + If the agent integrates with the Gemini Enterprise Agent Registry (A2A), you must inject the `a2a_agent_card` JSON metadata as a string **specifically inside the `async_create_session` method definition**: + + Example for A2A-Enabled ADK: + ```hcl + locals { + # Construct the A2A endpoint URL + a2a_url = "https://us-central1-aiplatform.googleapis.com/v1/projects/my-project/locations/us-central1/reasoningEngines/my-agent/a2a" + + agent_card = { + name = "my-agent" + description = "A2A Agent" + version = "1.0.0" + preferred_transport = "HTTP_JSON" + supported_interfaces = [{ url = local.a2a_url, protocol_binding = "HTTP_JSON" }] + capabilities = { streaming = true } + } + } + + # In class_methods, append "a2a_agent_card" key ONLY to the "async_create_session" method: + class_methods = jsonencode([ + # ... other 10 standard methods (same as Standard ADK) ... + { + name = "async_create_session" + api_mode = "async" + description = "Create a new session asynchronously" + parameters = { + type = "object" + required = ["user_id"] + properties = { + user_id = { type = "string" } + session_id = { type = "string" } + state = { type = "object" } + } + } + # Inject the serialized Agent Card here + a2a_agent_card = jsonencode(local.agent_card) + } + ]) + ``` state_func: 'func(v interface{}) string { s, _ := structure.NormalizeJsonString(v); return s }' custom_flatten: 'templates/terraform/custom_flatten/json_schema.tmpl' custom_expand: 'templates/terraform/custom_expand/json_value.tmpl' From 4c5858c0c6c8966fc820707ba28ef6108801bea0 Mon Sep 17 00:00:00 2001 From: Ricard Gardella Garcia Date: Thu, 9 Jul 2026 23:13:45 +0200 Subject: [PATCH 75/78] fix (compute): `enable_proxy_protocol` not being disableable on `google_compute_service_attachment`. (#18194) --- mmv1/products/compute/ServiceAttachment.yaml | 1 + ...esource_compute_service_attachment_test.go | 32 ++++++++++++++++--- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/mmv1/products/compute/ServiceAttachment.yaml b/mmv1/products/compute/ServiceAttachment.yaml index 6672d31599d8..372c7135f21d 100644 --- a/mmv1/products/compute/ServiceAttachment.yaml +++ b/mmv1/products/compute/ServiceAttachment.yaml @@ -262,6 +262,7 @@ properties: address data in TCP connections that traverse proxies on their way to destination servers. required: true + send_empty_value: true - name: 'domainNames' type: Array description: | diff --git a/mmv1/third_party/terraform/services/compute/resource_compute_service_attachment_test.go b/mmv1/third_party/terraform/services/compute/resource_compute_service_attachment_test.go index cf07b80f0233..4d2f070923ca 100644 --- a/mmv1/third_party/terraform/services/compute/resource_compute_service_attachment_test.go +++ b/mmv1/third_party/terraform/services/compute/resource_compute_service_attachment_test.go @@ -35,7 +35,7 @@ func TestAccComputeServiceAttachment_serviceAttachmentBasicExampleUpdate(t *test ImportStateVerifyIgnore: []string{"target_service", "region"}, }, { - Config: testAccComputeServiceAttachment_serviceAttachmentBasicExampleUpdate(context, true, -1), + Config: testAccComputeServiceAttachment_serviceAttachmentBasicExampleUpdate(context, true, -1, true), }, { ResourceName: "google_compute_service_attachment.psc_ilb_service_attachment", @@ -44,7 +44,7 @@ func TestAccComputeServiceAttachment_serviceAttachmentBasicExampleUpdate(t *test ImportStateVerifyIgnore: []string{"target_service", "region"}, }, { - Config: testAccComputeServiceAttachment_serviceAttachmentBasicExampleUpdate(context, false, -1), + Config: testAccComputeServiceAttachment_serviceAttachmentBasicExampleUpdate(context, false, -1, true), }, { ResourceName: "google_compute_service_attachment.psc_ilb_service_attachment", @@ -53,13 +53,33 @@ func TestAccComputeServiceAttachment_serviceAttachmentBasicExampleUpdate(t *test ImportStateVerifyIgnore: []string{"target_service", "region"}, }, { - Config: testAccComputeServiceAttachment_serviceAttachmentBasicExampleUpdate(context, false, 0), + Config: testAccComputeServiceAttachment_serviceAttachmentBasicExampleUpdate(context, false, 0, true), ConfigPlanChecks: resource.ConfigPlanChecks{ PreApply: []plancheck.PlanCheck{ plancheck.ExpectNonEmptyPlan(), }, }, }, + { + // Regression test: disabling enable_proxy_protocol after it was enabled must + // actually be sent to the API (it's a zero-valued bool), not silently dropped. + Config: testAccComputeServiceAttachment_serviceAttachmentBasicExampleUpdate(context, false, 0, false), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("google_compute_service_attachment.psc_ilb_service_attachment", plancheck.ResourceActionUpdate), + }, + }, + Check: resource.TestCheckResourceAttr( + "google_compute_service_attachment.psc_ilb_service_attachment", "enable_proxy_protocol", "false"), + }, + { + ResourceName: "google_compute_service_attachment.psc_ilb_service_attachment", + ImportState: true, + ImportStateVerify: true, + // send_propagated_connection_limit_if_zero is a virtual field with no API + // representation, so it can't be reconstructed on import. + ImportStateVerifyIgnore: []string{"target_service", "region", "send_propagated_connection_limit_if_zero"}, + }, }, }) } @@ -342,7 +362,7 @@ resource "google_compute_subnetwork" "psc_ilb_nat" { `, context) } -func testAccComputeServiceAttachment_serviceAttachmentBasicExampleUpdate(context map[string]interface{}, preventDestroy bool, propagatedConnectionLimit int) string { +func testAccComputeServiceAttachment_serviceAttachmentBasicExampleUpdate(context map[string]interface{}, preventDestroy bool, propagatedConnectionLimit int, enableProxyProtocol bool) string { context["lifecycle_block"] = "" if preventDestroy { context["lifecycle_block"] = ` @@ -351,6 +371,8 @@ func testAccComputeServiceAttachment_serviceAttachmentBasicExampleUpdate(context }` } + context["enable_proxy_protocol"] = enableProxyProtocol + switch { case propagatedConnectionLimit == 0: context["propagated_connection_limit"] = ` @@ -369,7 +391,7 @@ resource "google_compute_service_attachment" "psc_ilb_service_attachment" { region = "us-west2" description = "A service attachment configured with Terraforms" - enable_proxy_protocol = true + enable_proxy_protocol = %{enable_proxy_protocol} connection_preference = "ACCEPT_MANUAL" nat_subnets = [google_compute_subnetwork.psc_ilb_nat.id] target_service = google_compute_forwarding_rule.psc_ilb_target_service.id From 2b7627be94e045c0b78a163ac9ab6d5d03e0fb21 Mon Sep 17 00:00:00 2001 From: Riley Karson Date: Thu, 9 Jul 2026 15:48:09 -0700 Subject: [PATCH 76/78] Add shuyama1 to reviewers (#18244) --- .ci/magician/github/membership_data.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.ci/magician/github/membership_data.go b/.ci/magician/github/membership_data.go index f49fea6cb403..f96a682879b2 100644 --- a/.ci/magician/github/membership_data.go +++ b/.ci/magician/github/membership_data.go @@ -144,6 +144,14 @@ var ( }, }, }, + "shuyama1": { + vacations: []Vacation{ + { + startDate: newDate(2026, 07, 28), + endDate: newDate(2026, 12, 31), + }, + }, + }, "SirGitsalot": { vacations: []Vacation{ { @@ -172,7 +180,6 @@ var ( "tavasyag": {}, "hao-nan-li": {}, "NickElliot": {}, - "shuyama1": {}, "trodge": {}, "zli82016": {}, "vr-ibm": {}, From 28117359dfdcbf7fe64dca4ed017bae14f67b352 Mon Sep 17 00:00:00 2001 From: Ashutosh Saini Date: Fri, 10 Jul 2026 04:39:10 +0530 Subject: [PATCH 77/78] Bigquery tvf submission (#17892) --- .../bigqueryanalyticshub/QueryTemplate.yaml | 158 ++++++++++++++++++ ...query_analytics_hub_query_template.go.tmpl | 13 ++ ...query_analytics_hub_query_template.go.tmpl | 37 ++++ ...query_analytics_hub_query_template.go.tmpl | 28 ++++ ...query_analytics_hub_query_template.go.tmpl | 29 ++++ ...y_analyticshub_querytemplate_basic.tf.tmpl | 30 ++++ 6 files changed, 295 insertions(+) create mode 100644 mmv1/products/bigqueryanalyticshub/QueryTemplate.yaml create mode 100644 mmv1/templates/terraform/constants/bigquery_analytics_hub_query_template.go.tmpl create mode 100644 mmv1/templates/terraform/custom_delete/bigquery_analytics_hub_query_template.go.tmpl create mode 100644 mmv1/templates/terraform/post_create/bigquery_analytics_hub_query_template.go.tmpl create mode 100644 mmv1/templates/terraform/post_update/bigquery_analytics_hub_query_template.go.tmpl create mode 100644 mmv1/templates/terraform/samples/services/bigqueryanalyticshub/bigquery_analyticshub_querytemplate_basic.tf.tmpl diff --git a/mmv1/products/bigqueryanalyticshub/QueryTemplate.yaml b/mmv1/products/bigqueryanalyticshub/QueryTemplate.yaml new file mode 100644 index 000000000000..3959477dea5e --- /dev/null +++ b/mmv1/products/bigqueryanalyticshub/QueryTemplate.yaml @@ -0,0 +1,158 @@ +# Copyright 2025 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: "QueryTemplate" +min_version: beta +deletion_policy_default: "DELETE_IF_DRAFTED" +deletion_policy_custom_docs: true +description: | + Represents a BigQuery Query Template within a Data Exchange. + This resource defines a reusable SQL routine (e.g., a TVF) that can be + shared or executed via the Data Exchange. +references: + guides: + "Use query templates": "https://docs.cloud.google.com/bigquery/docs/query-templates" + api: "https://docs.cloud.google.com/bigquery/docs/reference/analytics-hub/rest/v1/projects.locations.dataExchanges.queryTemplates" +self_link: "projects/{{project}}/locations/{{location}}/dataExchanges/{{data_exchange_id}}/queryTemplates/{{query_template_id}}" +id_format: "projects/{{project}}/locations/{{location}}/dataExchanges/{{data_exchange_id}}/queryTemplates/{{query_template_id}}" +create_url: "projects/{{project}}/locations/{{location}}/dataExchanges/{{data_exchange_id}}/queryTemplates?queryTemplateId={{query_template_id}}" +base_url: "projects/{{project}}/locations/{{location}}/dataExchanges/{{data_exchange_id}}/queryTemplates" +update_verb: "PATCH" +update_mask: true +import_format: + - "projects/{{project}}/locations/{{location}}/dataExchanges/{{data_exchange_id}}/queryTemplates/{{query_template_id}}" +timeouts: + insert_minutes: 20 + update_minutes: 20 + delete_minutes: 20 +samples: + - name: "bigquery_analyticshub_querytemplate_basic" + primary_resource_id: "querytemplate" + region_override: "us" + steps: + - name: "bigquery_analyticshub_querytemplate_basic" + resource_id_vars: + data_exchange_id: "my_data_exchange" + query_template_id: "my_query_template" + vars: + desc: "example of query template" + table_name: "t1" + submit: "false" + ignore_read_extra: + - "submit" + - "deletion_policy" + - name: "bigquery_analyticshub_querytemplate_basic" + resource_id_vars: + data_exchange_id: "my_data_exchange" + query_template_id: "my_query_template" + vars: + desc: "updated example of query template" + table_name: "t1updated" + submit: "true" + ignore_read_extra: + - "submit" + - "deletion_policy" + +custom_code: + constants: "templates/terraform/constants/bigquery_analytics_hub_query_template.go.tmpl" + post_update: "templates/terraform/post_update/bigquery_analytics_hub_query_template.go.tmpl" + post_create: "templates/terraform/post_create/bigquery_analytics_hub_query_template.go.tmpl" + custom_delete: "templates/terraform/custom_delete/bigquery_analytics_hub_query_template.go.tmpl" +custom_diff: + - 'resourceBigqueryAnalyticsHubQueryTemplateCustomDiff' +virtual_fields: + - name: "submit" + type: Boolean + description: "If set to `true`, the QueryTemplate will be submitted for approval and cannot be updated afterwards. This is a one-time action." + - name: "deletion_policy" + type: String + exclude: true + description: | + Sets the policy for deleting the QueryTemplate. Defaults to `DELETE_IF_DRAFTED`. + * `ABANDON`: Untracks the resource from Terraform state but leaves it intact in BigQuery. + * `DELETE`: Deletes the QueryTemplate from BigQuery. + * `DELETE_IF_DRAFTED`: Deletes the QueryTemplate only if it is in a `DRAFTED` state; otherwise, it abandons it. + * `PREVENT`: Prevents deletion of the QueryTemplate. +properties: + - name: "name" + type: String + description: "The resource name of the QueryTemplate. e.g. `projects/myproject/locations/us/dataExchanges/123/queryTemplates/456`" + output: true + - name: "location" + type: String + description: "The name of the location this data exchange query template." + required: true + url_param_only: true + immutable: true + - name: "data_exchange_id" + type: String + description: "The ID of the data exchange. Must contain only Unicode letters, numbers (0-9), underscores (_). Should not use characters that require URL-escaping, or characters outside of ASCII, spaces." + required: true + url_param_only: true + immutable: true + - name: "query_template_id" + type: String + description: "Unique QueryTemplate ID." + required: true + url_param_only: true + immutable: true + - name: "displayName" + type: String + description: | + Human-readable display name of the QueryTemplate. The display name must + contain only Unicode letters, numbers (0-9), underscores (_), dashes (-), + spaces ( ), ampersands (&) and can't start or end with spaces. Default + value is an empty string. + required: true + immutable: true + - name: "description" + type: String + description: | + Short description of the QueryTemplate. The description must not contain + Unicode non-characters and C0 and C1 control codes except tabs, + new lines, carriage returns, and page breaks. + Default value is an empty string. Max length: 2000 bytes. + - name: "primaryContact" + type: String + description: | + Email or URL of the primary point of contact of the QueryTemplate. + immutable: true + - name: "documentation" + type: String + description: "Documentation describing the QueryTemplate." + immutable: true + - name: "state" + type: String + description: "The QueryTemplate lifecycle state." + output: true + - name: "routine" + type: NestedObject + description: "The routine associated with the QueryTemplate." + properties: + - name: "routineType" + type: Enum + description: "Type of routine (e.g., TABLE_VALUED_FUNCTION)." + enum_values: + - "TABLE_VALUED_FUNCTION" + - name: "definitionBody" + type: String + description: "SQL query logic." + - name: "createTime" + type: String + description: "Timestamp when the QueryTemplate was created." + output: true + - name: "updateTime" + type: String + description: "Timestamp when the QueryTemplate was last modified." + output: true diff --git a/mmv1/templates/terraform/constants/bigquery_analytics_hub_query_template.go.tmpl b/mmv1/templates/terraform/constants/bigquery_analytics_hub_query_template.go.tmpl new file mode 100644 index 000000000000..a41e7699a71e --- /dev/null +++ b/mmv1/templates/terraform/constants/bigquery_analytics_hub_query_template.go.tmpl @@ -0,0 +1,13 @@ +func resourceBigqueryAnalyticsHubQueryTemplateCustomDiffFunc(diff tpgresource.TerraformResourceDiff) error { + if diff.HasChange("submit") { + old, new := diff.GetChange("submit") + if !new.(bool) && old.(bool) { + return fmt.Errorf("Cannot change `submit` from true to false as unsubmitting is not supported.") + } + } + return nil +} + +func resourceBigqueryAnalyticsHubQueryTemplateCustomDiff(_ context.Context, diff *schema.ResourceDiff, meta interface{}) error { + return resourceBigqueryAnalyticsHubQueryTemplateCustomDiffFunc(diff) +} \ No newline at end of file diff --git a/mmv1/templates/terraform/custom_delete/bigquery_analytics_hub_query_template.go.tmpl b/mmv1/templates/terraform/custom_delete/bigquery_analytics_hub_query_template.go.tmpl new file mode 100644 index 000000000000..b85d090923d3 --- /dev/null +++ b/mmv1/templates/terraform/custom_delete/bigquery_analytics_hub_query_template.go.tmpl @@ -0,0 +1,37 @@ +deletionPolicy := d.Get("deletion_policy").(string) + +url, err := tpgresource.ReplaceVars(d, config, "{{"{{"}}BigqueryAnalyticsHubBasePath{{"}}"}}projects/{{"{{"}}project{{"}}"}}/locations/{{"{{"}}location{{"}}"}}/dataExchanges/{{"{{"}}data_exchange_id{{"}}"}}/queryTemplates/{{"{{"}}query_template_id{{"}}"}}") +if err != nil { + return err +} + +billingProject := "" +if bp, err := tpgresource.GetBillingProject(d, config); err == nil { + billingProject = bp +} + +var obj map[string]interface{} +log.Printf("[DEBUG] Deleting QueryTemplate %q", d.Id()) + +// Send the Delete Request +res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "DELETE", + Project: billingProject, + RawURL: url, + UserAgent: userAgent, + Body: obj, + Timeout: d.Timeout(schema.TimeoutDelete), +}) + +if err != nil { + if deletionPolicy == "DELETE_IF_DRAFTED" && transport_tpg.IsGoogleApiErrorWithCode(err, 400) && strings.Contains(err.Error(), "must be in DRAFTED state") { + log.Printf("[WARN] QueryTemplate %q is not in DRAFTED state. Abandoning resource due to deletion_policy 'DELETE_IF_DRAFTED'.", d.Id()) + d.SetId("") + return nil + } + return transport_tpg.HandleNotFoundError(err, d, "QueryTemplate") +} + +log.Printf("[DEBUG] Finished deleting QueryTemplate %q: %#v", d.Id(), res) +return nil \ No newline at end of file diff --git a/mmv1/templates/terraform/post_create/bigquery_analytics_hub_query_template.go.tmpl b/mmv1/templates/terraform/post_create/bigquery_analytics_hub_query_template.go.tmpl new file mode 100644 index 000000000000..d22139ba046e --- /dev/null +++ b/mmv1/templates/terraform/post_create/bigquery_analytics_hub_query_template.go.tmpl @@ -0,0 +1,28 @@ +// After successful creation, check and perform submit if requested +if d.Get("submit").(bool) { + log.Printf("[DEBUG] Attempting to submit BigQuery Analytics Hub QueryTemplate %q (ID: %s)", d.Get("name").(string), d.Id()) + + submitUrl, err := tpgresource.ReplaceVars(d, config, "{{"{{"}}BigqueryAnalyticsHubBasePath{{"}}"}}projects/{{"{{"}}project{{"}}"}}/locations/{{"{{"}}location{{"}}"}}/dataExchanges/{{"{{"}}data_exchange_id{{"}}"}}/queryTemplates/{{"{{"}}query_template_id{{"}}"}}:submit") + if err != nil { + return fmt.Errorf("Error constructing submit URL for QueryTemplate %q: %s", d.Id(), err) + } + + submitBodyMap := map[string]interface{}{} + submitHeaders := make(http.Header) + submitHeaders.Set("Content-Type", "application/json") + + _, err = transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "POST", + Project: billingProject, + RawURL: submitUrl, + UserAgent: userAgent, + Headers: submitHeaders, + Body: submitBodyMap, + }) + + if err != nil { + return fmt.Errorf("Error submitting QueryTemplate %q: %s", d.Id(), err) + } + log.Printf("[DEBUG] Successfully submitted QueryTemplate %q", d.Id()) +} \ No newline at end of file diff --git a/mmv1/templates/terraform/post_update/bigquery_analytics_hub_query_template.go.tmpl b/mmv1/templates/terraform/post_update/bigquery_analytics_hub_query_template.go.tmpl new file mode 100644 index 000000000000..a26e9715ba5b --- /dev/null +++ b/mmv1/templates/terraform/post_update/bigquery_analytics_hub_query_template.go.tmpl @@ -0,0 +1,29 @@ +// After successful update, check and perform submit if requested +if d.Get("submit").(bool) { + // If the update succeeded, we can assume the state is DRAFTED + log.Printf("[DEBUG] Attempting to submit BigQuery Analytics Hub QueryTemplate %q (ID: %s)", d.Get("name").(string), d.Id()) + + submitUrl, err := tpgresource.ReplaceVars(d, config, "{{"{{"}}BigqueryAnalyticsHubBasePath{{"}}"}}projects/{{"{{"}}project{{"}}"}}/locations/{{"{{"}}location{{"}}"}}/dataExchanges/{{"{{"}}data_exchange_id{{"}}"}}/queryTemplates/{{"{{"}}query_template_id{{"}}"}}:submit") + if err != nil { + return fmt.Errorf("Error constructing submit URL for QueryTemplate %q: %s", d.Id(), err) + } + + submitBodyMap := map[string]interface{}{} + submitHeaders := make(http.Header) + submitHeaders.Set("Content-Type", "application/json") + + _, err = transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "POST", + Project: billingProject, + RawURL: submitUrl, + UserAgent: userAgent, + Headers: submitHeaders, + Body: submitBodyMap, + }) + + if err != nil { + return fmt.Errorf("Error submitting QueryTemplate %q: %s", d.Id(), err) + } + log.Printf("[DEBUG] Successfully submitted QueryTemplate %q", d.Id()) +} \ No newline at end of file diff --git a/mmv1/templates/terraform/samples/services/bigqueryanalyticshub/bigquery_analyticshub_querytemplate_basic.tf.tmpl b/mmv1/templates/terraform/samples/services/bigqueryanalyticshub/bigquery_analyticshub_querytemplate_basic.tf.tmpl new file mode 100644 index 000000000000..110fb2946971 --- /dev/null +++ b/mmv1/templates/terraform/samples/services/bigqueryanalyticshub/bigquery_analyticshub_querytemplate_basic.tf.tmpl @@ -0,0 +1,30 @@ +data "google_client_openid_userinfo" "me" { + provider = google-beta +} + +resource "google_bigquery_analytics_hub_data_exchange" "{{$.PrimaryResourceId}}" { + provider = google-beta + display_name = "My Audience Data Exchange" + data_exchange_id = "{{index $.ResourceIdVars "data_exchange_id"}}" + description = "{{index $.Vars "desc"}}" + location = "us" + sharing_environment_config { + dcr_exchange_config {} + } +} + +resource "google_bigquery_analytics_hub_query_template" "{{$.PrimaryResourceId}}" { + provider = google-beta + location = "us" + data_exchange_id = google_bigquery_analytics_hub_data_exchange.{{$.PrimaryResourceId}}.data_exchange_id + query_template_id = "{{index $.ResourceIdVars "query_template_id"}}" + display_name = "{{index $.ResourceIdVars "query_template_id"}}" + description = "{{index $.Vars "desc"}}" + primary_contact = data.google_client_openid_userinfo.me.email + documentation = "This TVF takes a table t1 as input and returns all columns. Useful for basic data pass-through." + routine { + routine_type="TABLE_VALUED_FUNCTION" + definition_body="{{index $.ResourceIdVars "query_template_id"}}() as (select * from {{index $.Vars "table_name"}})" + } + submit={{index $.Vars "submit"}} +} From d023ea52e7bc8cda427bc7176e176715e594e131 Mon Sep 17 00:00:00 2001 From: Cameron Thornton Date: Thu, 9 Jul 2026 18:22:37 -0500 Subject: [PATCH 78/78] Update review vacations for c2thorn --- .ci/magician/github/membership_data.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.ci/magician/github/membership_data.go b/.ci/magician/github/membership_data.go index f96a682879b2..7020f731b692 100644 --- a/.ci/magician/github/membership_data.go +++ b/.ci/magician/github/membership_data.go @@ -83,6 +83,10 @@ var ( startDate: newDate(2026, 4, 19), endDate: newDate(2026, 4, 26), }, + { + startDate: newDate(2026, 7, 10), + endDate: newDate(2026, 7, 17), + }, }, }, "malhotrasagar2212": {