Repository: GoogleCloudPlatform/terraform-validator
Pull request #1128 (2022-11-16 18:13:13)

Fetch project with storage API for storage bucket
https://github.com/GoogleCloudPlatform/terraform-validator/issues/1092

Fetch project with storage API if cai Resource Data does not contain project. Falls back to provider config project if fetching from API fails. 

Thought about adding an intergration test, but it requires the bucket exist to make it work, so just added a unit test. 


Patch:
diff --git a/ancestrymanager/ancestrymanager.go b/ancestrymanager/ancestrymanager.go
index 7961cbd6..30ea1c41 100644
--- a/ancestrymanager/ancestrymanager.go
+++ b/ancestrymanager/ancestrymanager.go
@@ -3,10 +3,12 @@ package ancestrymanager
 
 import (
 	"fmt"
+	"strconv"
 	"strings"
 
 	crmv1 "google.golang.org/api/cloudresourcemanager/v1"
 	crmv3 "google.golang.org/api/cloudresourcemanager/v3"
+	"google.golang.org/api/storage/v1"
 
 	resources "github.com/GoogleCloudPlatform/terraform-validator/converters/google/resources"
 
@@ -28,6 +30,7 @@ type manager struct {
 	// be disabled.
 	resourceManagerV3 *crmv3.Service
 	resourceManagerV1 *crmv1.Service
+	storageClient     *storage.Service
 	// Cache to prevent multiple network calls for looking up the same
 	// resource's ancestry. The map key is the resource itself, in the format of
 	// "<type>/<id>", ancestors are sorted from closest to furthest.
@@ -47,6 +50,7 @@ func New(cfg *resources.Config, offline bool, entries map[string]string, errorLo
 	if !offline {
 		am.resourceManagerV1 = cfg.NewResourceManagerClient(cfg.UserAgent())
 		am.resourceManagerV3 = cfg.NewResourceManagerV3Client(cfg.UserAgent())
+		am.storageClient = cfg.NewStorageClient(cfg.UserAgent())
 	}
 	err := am.initAncestryCache(entries)
 	if err != nil {
@@ -132,7 +136,7 @@ func (m *manager) fetchAncestors(config *resources.Config, tfData resources.Terr
 			folderKey = fmt.Sprintf("folders/%s", folderKey)
 		}
 	}
-	project, _ := getProjectFromResource(tfData, config, *cai, m.errorLogger)
+	project, _ := m.getProjectFromResource(tfData, config, cai)
 	if project != "" {
 		projectKey = project
 		if !strings.HasPrefix(projectKey, "projects/") {
@@ -308,6 +312,50 @@ func normalizeAncestry(val string) string {
 	return val
 }
 
+// getProjectFromResource reads the "project" field from the given resource data and falls
+// back to the provider's value if not given. If the provider's value is not
+// given, an error is returned.
+func (m *manager) getProjectFromResource(d resources.TerraformResourceData, config *resources.Config, cai *resources.Asset) (string, error) {
+
+	switch cai.Type {
+	case "cloudresourcemanager.googleapis.com/Project",
+		"cloudbilling.googleapis.com/ProjectBillingInfo":
+		res, ok := d.GetOk("number")
+		if ok {
+			return res.(string), nil
+		}
+		// Fall back to project_id if number is not available.
+		res, ok = d.GetOk("project_id")
+		if ok {
+			return res.(string), nil
+		} else {
+			m.errorLogger.Warn(fmt.Sprintf("Failed to retrieve project_id for %s from resource", cai.Name))
+		}
+	case "storage.googleapis.com/Bucket":
+		if cai.Resource != nil {
+			res, ok := cai.Resource.Data["project"]
+			if ok {
+				return res.(string), nil
+			}
+		}
+		m.errorLogger.Warn(fmt.Sprintf("Failed to retrieve project_id for %s from cai resource", cai.Name))
+
+		bucketField, ok := d.GetOk("bucket")
+		if ok {
+			bucket := bucketField.(string)
+			resp, err := m.storageClient.Buckets.Get(bucket).Do()
+			if err == nil {
+				projectNum := resp.ProjectNumber
+				return strconv.Itoa(int(projectNum)), nil
+			}
+			m.errorLogger.Warn(fmt.Sprintf("Failed to get bucket %s", bucket))
+		}
+		m.errorLogger.Warn("Failed to retrieve bucket field from tf data")
+	}
+
+	return getProjectFromSchema("project", d, config)
+}
+
 type NoOpAncestryManager struct{}
 
 func (*NoOpAncestryManager) Ancestors(config *resources.Config, tfData resources.TerraformResourceData, cai *resources.Asset) ([]string, string, error) {
diff --git a/ancestrymanager/ancestryutil.go b/ancestrymanager/ancestryutil.go
index c5a28a67..7a92b3f3 100644
--- a/ancestrymanager/ancestryutil.go
+++ b/ancestrymanager/ancestryutil.go
@@ -7,8 +7,6 @@ import (
 	resources "github.com/GoogleCloudPlatform/terraform-validator/converters/google/resources"
 	"github.com/hashicorp/errwrap"
 	"google.golang.org/api/googleapi"
-
-	"go.uber.org/zap"
 )
 
 // assetParent derives a resource's parent from its ancestors.
@@ -76,38 +74,6 @@ func sanitizeAncestryPath(s string) string {
 	return ret
 }
 
-// getProjectFromResource reads the "project" field from the given resource data and falls
-// back to the provider's value if not given. If the provider's value is not
-// given, an error is returned.
-func getProjectFromResource(d resources.TerraformResourceData, config *resources.Config, cai resources.Asset, errorLogger *zap.Logger) (string, error) {
-
-	switch cai.Type {
-	case "cloudresourcemanager.googleapis.com/Project",
-		"cloudbilling.googleapis.com/ProjectBillingInfo":
-		res, ok := d.GetOk("number")
-		if ok {
-			return res.(string), nil
-		}
-		// Fall back to project_id if number is not available.
-		res, ok = d.GetOk("project_id")
-		if ok {
-			return res.(string), nil
-		} else {
-			errorLogger.Warn(fmt.Sprintf("Failed to retrieve project_id for %s from resource", cai.Name))
-		}
-	case "storage.googleapis.com/Bucket":
-		if cai.Resource != nil {
-			res, ok := cai.Resource.Data["project"]
-			if ok {
-				return res.(string), nil
-			}
-		}
-		errorLogger.Warn(fmt.Sprintf("Failed to retrieve project_id for %s from cai resource", cai.Name))
-	}
-
-	return getProjectFromSchema("project", d, config)
-}
-
 func getProjectFromSchema(projectSchemaField string, d resources.TerraformResourceData, config *resources.Config) (string, error) {
 	res, ok := d.GetOk(projectSchemaField)
 	if ok && projectSchemaField != "" {
diff --git a/converters/google/resources/compute_backend_service.go b/converters/google/resources/compute_backend_service.go
index ef1b009a..75cf287b 100644
--- a/converters/google/resources/compute_backend_service.go
+++ b/converters/google/resources/compute_backend_service.go
@@ -843,6 +843,13 @@ func expandComputeBackendServiceCdnPolicyCacheKeyPolicy(v interface{}, d Terrafo
 		transformed["queryStringWhitelist"] = transformedQueryStringWhitelist
 	}
 
+	transformedIncludeHttpHeaders, err := expandComputeBackendServiceCdnPolicyCacheKeyPolicyIncludeHttpHeaders(original["include_http_headers"], d, config)
+	if err != nil {
+		return nil, err
+	} else {
+		transformed["includeHttpHeaders"] = transformedIncludeHttpHeaders
+	}
+
 	transformedIncludeNamedCookies, err := expandComputeBackendServiceCdnPolicyCacheKeyPolicyIncludeNamedCookies(original["include_named_cookies"], d, config)
 	if err != nil {
 		return nil, err
@@ -875,6 +882,10 @@ func expandComputeBackendServiceCdnPolicyCacheKeyPolicyQueryStringWhitelist(v in
 	return v, nil
 }
 
+func expandComputeBackendServiceCdnPolicyCacheKeyPolicyIncludeHttpHeaders(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) {
+	return v, nil
+}
+
 func expandComputeBackendServiceCdnPolicyCacheKeyPolicyIncludeNamedCookies(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) {
 	return v, nil
 }
diff --git a/converters/google/resources/compute_region_url_map.go b/converters/google/resources/compute_region_url_map.go
index d29d6dcc..0873e2ed 100644
--- a/converters/google/resources/compute_region_url_map.go
+++ b/converters/google/resources/compute_region_url_map.go
@@ -2556,6 +2556,20 @@ func expandComputeRegionUrlMapDefaultRouteAction(v interface{}, d TerraformResou
 		transformed["weightedBackendServices"] = transformedWeightedBackendServices
 	}
 
+	transformedUrlRewrite, err := expandComputeRegionUrlMapDefaultRouteActionUrlRewrite(original["url_rewrite"], d, config)
+	if err != nil {
+		return nil, err
+	} else if val := reflect.ValueOf(transformedUrlRewrite); val.IsValid() && !isEmptyValue(val) {
+		transformed["urlRewrite"] = transformedUrlRewrite
+	}
+
+	transformedTimeout, err := expandComputeRegionUrlMapDefaultRouteActionTimeout(original["timeout"], d, config)
+	if err != nil {
+		return nil, err
+	} else if val := reflect.ValueOf(transformedTimeout); val.IsValid() && !isEmptyValue(val) {
+		transformed["timeout"] = transformedTimeout
+	}
+
 	transformedRetryPolicy, err := expandComputeRegionUrlMapDefaultRouteActionRetryPolicy(original["retry_policy"], d, config)
 	if err != nil {
 		return nil, err
@@ -2570,6 +2584,20 @@ func expandComputeRegionUrlMapDefaultRouteAction(v interface{}, d TerraformResou
 		transformed["requestMirrorPolicy"] = transformedRequestMirrorPolicy
 	}
 
+	transformedCorsPolicy, err := expandComputeRegionUrlMapDefaultRouteActionCorsPolicy(original["cors_policy"], d, config)
+	if err != nil {
+		return nil, err
+	} else if val := reflect.ValueOf(transformedCorsPolicy); val.IsValid() && !isEmptyValue(val) {
+		transformed["corsPolicy"] = transformedCorsPolicy
+	}
+
+	transformedFaultInjectionPolicy, err := expandComputeRegionUrlMapDefaultRouteActionFaultInjectionPolicy(original["fault_injection_policy"], d, config)
+	if err != nil {
+		return nil, err
+	} else if val := reflect.ValueOf(transformedFaultInjectionPolicy); val.IsValid() && !isEmptyValue(val) {
+		transformed["faultInjectionPolicy"] = transformedFaultInjectionPolicy
+	}
+
 	return transformed, nil
 }
 
@@ -2765,6 +2793,74 @@ func expandComputeRegionUrlMapDefaultRouteActionWeightedBackendServicesHeaderAct
 	return v, nil
 }
 
+func expandComputeRegionUrlMapDefaultRouteActionUrlRewrite(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) {
+	l := v.([]interface{})
+	if len(l) == 0 || l[0] == nil {
+		return nil, nil
+	}
+	raw := l[0]
+	original := raw.(map[string]interface{})
+	transformed := make(map[string]interface{})
+
+	transformedPathPrefixRewrite, err := expandComputeRegionUrlMapDefaultRouteActionUrlRewritePathPrefixRewrite(original["path_prefix_rewrite"], d, config)
+	if err != nil {
+		return nil, err
+	} else if val := reflect.ValueOf(transformedPathPrefixRewrite); val.IsValid() && !isEmptyValue(val) {
+		transformed["pathPrefixRewrite"] = transformedPathPrefixRewrite
+	}
+
+	transformedHostRewrite, err := expandComputeRegionUrlMapDefaultRouteActionUrlRewriteHostRewrite(original["host_rewrite"], d, config)
+	if err != nil {
+		return nil, err
+	} else if val := reflect.ValueOf(transformedHostRewrite); val.IsValid() && !isEmptyValue(val) {
+		transformed["hostRewrite"] = transformedHostRewrite
+	}
+
+	return transformed, nil
+}
+
+func expandComputeRegionUrlMapDefaultRouteActionUrlRewritePathPrefixRewrite(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) {
+	return v, nil
+}
+
+func expandComputeRegionUrlMapDefaultRouteActionUrlRewriteHostRewrite(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) {
+	return v, nil
+}
+
+func expandComputeRegionUrlMapDefaultRouteActionTimeout(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) {
+	l := v.([]interface{})
+	if len(l) == 0 || l[0] == nil {
+		return nil, nil
+	}
+	raw := l[0]
+	original := raw.(map[string]interface{})
+	transformed := make(map[string]interface{})
+
+	transformedSeconds, err := expandComputeRegionUrlMapDefaultRouteActionTimeoutSeconds(original["seconds"], d, config)
+	if err != nil {
+		return nil, err
+	} else if val := reflect.ValueOf(transformedSeconds); val.IsValid() && !isEmptyValue(val) {
+		transformed["seconds"] = transformedSeconds
+	}
+
+	transformedNanos, err := expandComputeRegionUrlMapDefaultRouteActionTimeoutNanos(original["nanos"], d, config)
+	if err != nil {
+		return nil, err
+	} else if val := reflect.ValueOf(transformedNanos); val.IsValid() && !isEmptyValue(val) {
+		transformed["nanos"] = transformedNanos
+	}
+
+	return transformed, nil
+}
+
+func expandComputeRegionUrlMapDefaultRouteActionTimeoutSeconds(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) {
+	return v, nil
+}
+
+func expandComputeRegionUrlMapDefaultRouteActionTimeoutNanos(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) {
+	return v, nil
+}
+
 func expandComputeRegionUrlMapDefaultRouteActionRetryPolicy(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) {
 	l := v.([]interface{})
 	if len(l) == 0 || l[0] == nil {
@@ -2867,6 +2963,230 @@ func expandComputeRegionUrlMapDefaultRouteActionRequestMirrorPolicyBackendServic
 	return f.RelativeLink(), nil
 }
 
+func expandComputeRegionUrlMapDefaultRouteActionCorsPolicy(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) {
+	l := v.([]interface{})
+	if len(l) == 0 || l[0] == nil {
+		return nil, nil
+	}
+	raw := l[0]
+	original := raw.(map[string]interface{})
+	transformed := make(map[string]interface{})
+
+	transformedAllowOrigins, err := expandComputeRegionUrlMapDefaultRouteActionCorsPolicyAllowOrigins(original["allow_origins"], d, config)
+	if err != nil {
+		return nil, err
+	} else if val := reflect.ValueOf(transformedAllowOrigins); val.IsValid() && !isEmptyValue(val) {
+		transformed["allowOrigins"] = transformedAllowOrigins
+	}
+
+	transformedAllowOriginRegexes, err := expandComputeRegionUrlMapDefaultRouteActionCorsPolicyAllowOriginRegexes(original["allow_origin_regexes"], d, config)
+	if err != nil {
+		return nil, err
+	} else if val := reflect.ValueOf(transformedAllowOriginRegexes); val.IsValid() && !isEmptyValue(val) {
+		transformed["allowOriginRegexes"] = transformedAllowOriginRegexes
+	}
+
+	transformedAllowMethods, err := expandComputeRegionUrlMapDefaultRouteActionCorsPolicyAllowMethods(original["allow_methods"], d, config)
+	if err != nil {
+		return nil, err
+	} else if val := reflect.ValueOf(transformedAllowMethods); val.IsValid() && !isEmptyValue(val) {
+		transformed["allowMethods"] = transformedAllowMethods
+	}
+
+	transformedAllowHeaders, err := expandComputeRegionUrlMapDefaultRouteActionCorsPolicyAllowHeaders(original["allow_headers"], d, config)
+	if err != nil {
+		return nil, err
+	} else if val := reflect.ValueOf(transformedAllowHeaders); val.IsValid() && !isEmptyValue(val) {
+		transformed["allowHeaders"] = transformedAllowHeaders
+	}
+
+	transformedExposeHeaders, err := expandComputeRegionUrlMapDefaultRouteActionCorsPolicyExposeHeaders(original["expose_headers"], d, config)
+	if err != nil {
+		return nil, err
+	} else if val := reflect.ValueOf(transformedExposeHeaders); val.IsValid() && !isEmptyValue(val) {
+		transformed["exposeHeaders"] = transformedExposeHeaders
+	}
+
+	transformedMaxAge, err := expandComputeRegionUrlMapDefaultRouteActionCorsPolicyMaxAge(original["max_age"], d, config)
+	if err != nil {
+		return nil, err
+	} else if val := reflect.ValueOf(transformedMaxAge); val.IsValid() && !isEmptyValue(val) {
+		transformed["maxAge"] = transformedMaxAge
+	}
+
+	transformedAllowCredentials, err := expandComputeRegionUrlMapDefaultRouteActionCorsPolicyAllowCredentials(original["allow_credentials"], d, config)
+	if err != nil {
+		return nil, err
+	} else if val := reflect.ValueOf(transformedAllowCredentials); val.IsValid() && !isEmptyValue(val) {
+		transformed["allowCredentials"] = transformedAllowCredentials
+	}
+
+	transformedDisabled, err := expandComputeRegionUrlMapDefaultRouteActionCorsPolicyDisabled(original["disabled"], d, config)
+	if err != nil {
+		return nil, err
+	} else if val := reflect.ValueOf(transformedDisabled); val.IsValid() && !isEmptyValue(val) {
+		transformed["disabled"] = transformedDisabled
+	}
+
+	return transformed, nil
+}
+
+func expandComputeRegionUrlMapDefaultRouteActionCorsPolicyAllowOrigins(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) {
+	return v, nil
+}
+
+func expandComputeRegionUrlMapDefaultRouteActionCorsPolicyAllowOriginRegexes(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) {
+	return v, nil
+}
+
+func expandComputeRegionUrlMapDefaultRouteActionCorsPolicyAllowMethods(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) {
+	return v, nil
+}
+
+func expandComputeRegionUrlMapDefaultRouteActionCorsPolicyAllowHeaders(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) {
+	return v, nil
+}
+
+func expandComputeRegionUrlMapDefaultRouteActionCorsPolicyExposeHeaders(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) {
+	return v, nil
+}
+
+func expandComputeRegionUrlMapDefaultRouteActionCorsPolicyMaxAge(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) {
+	return v, nil
+}
+
+func expandComputeRegionUrlMapDefaultRouteActionCorsPolicyAllowCredentials(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) {
+	return v, nil
+}
+
+func expandComputeRegionUrlMapDefaultRouteActionCorsPolicyDisabled(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) {
+	return v, nil
+}
+
+func expandComputeRegionUrlMapDefaultRouteActionFaultInjectionPolicy(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) {
+	l := v.([]interface{})
+	if len(l) == 0 || l[0] == nil {
+		return nil, nil
+	}
+	raw := l[0]
+	original := raw.(map[string]interface{})
+	transformed := make(map[string]interface{})
+
+	transformedDelay, err := expandComputeRegionUrlMapDefaultRouteActionFaultInjectionPolicyDelay(original["delay"], d, config)
+	if err != nil {
+		return nil, err
+	} else if val := reflect.ValueOf(transformedDelay); val.IsValid() && !isEmptyValue(val) {
+		transformed["delay"] = transformedDelay
+	}
+
+	transformedAbort, err := expandComputeRegionUrlMapDefaultRouteActionFaultInjectionPolicyAbort(original["abort"], d, config)
+	if err != nil {
+		return nil, err
+	} else if val := reflect.ValueOf(transformedAbort); val.IsValid() && !isEmptyValue(val) {
+		transformed["abort"] = transformedAbort
+	}
+
+	return transformed, nil
+}
+
+func expandComputeRegionUrlMapDefaultRouteActionFaultInjectionPolicyDelay(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) {
+	l := v.([]interface{})
+	if len(l) == 0 || l[0] == nil {
+		return nil, nil
+	}
+	raw := l[0]
+	original := raw.(map[string]interface{})
+	transformed := make(map[string]interface{})
+
+	transformedFixedDelay, err := expandComputeRegionUrlMapDefaultRouteActionFaultInjectionPolicyDelayFixedDelay(original["fixed_delay"], d, config)
+	if err != nil {
+		return nil, err
+	} else if val := reflect.ValueOf(transformedFixedDelay); val.IsValid() && !isEmptyValue(val) {
+		transformed["fixedDelay"] = transformedFixedDelay
+	}
+
+	transformedPercentage, err := expandComputeRegionUrlMapDefaultRouteActionFaultInjectionPolicyDelayPercentage(original["percentage"], d, config)
+	if err != nil {
+		return nil, err
+	} else if val := reflect.ValueOf(transformedPercentage); val.IsValid() && !isEmptyValue(val) {
+		transformed["percentage"] = transformedPercentage
+	}
+
+	return transformed, nil
+}
+
+func expandComputeRegionUrlMapDefaultRouteActionFaultInjectionPolicyDelayFixedDelay(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) {
+	l := v.([]interface{})
+	if len(l) == 0 || l[0] == nil {
+		return nil, nil
+	}
+	raw := l[0]
+	original := raw.(map[string]interface{})
+	transformed := make(map[string]interface{})
+
+	transformedSeconds, err := expandComputeRegionUrlMapDefaultRouteActionFaultInjectionPolicyDelayFixedDelaySeconds(original["seconds"], d, config)
+	if err != nil {
+		return nil, err
+	} else if val := reflect.ValueOf(transformedSeconds); val.IsValid() && !isEmptyValue(val) {
+		transformed["seconds"] = transformedSeconds
+	}
+
+	transformedNanos, err := expandComputeRegionUrlMapDefaultRouteActionFaultInjectionPolicyDelayFixedDelayNanos(original["nanos"], d, config)
+	if err != nil {
+		return nil, err
+	} else if val := reflect.ValueOf(transformedNanos); val.IsValid() && !isEmptyValue(val) {
+		transformed["nanos"] = transformedNanos
+	}
+
+	return transformed, nil
+}
+
+func expandComputeRegionUrlMapDefaultRouteActionFaultInjectionPolicyDelayFixedDelaySeconds(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) {
+	return v, nil
+}
+
+func expandComputeRegionUrlMapDefaultRouteActionFaultInjectionPolicyDelayFixedDelayNanos(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) {
+	return v, nil
+}
+
+func expandComputeRegionUrlMapDefaultRouteActionFaultInjectionPolicyDelayPercentage(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) {
+	return v, nil
+}
+
+func expandComputeRegionUrlMapDefaultRouteActionFaultInjectionPolicyAbort(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) {
+	l := v.([]interface{})
+	if len(l) == 0 || l[0] == nil {
+		return nil, nil
+	}
+	raw := l[0]
+	original := raw.(map[string]interface{})
+	transformed := make(map[string]interface{})
+
+	transformedHttpStatus, err := expandComputeRegionUrlMapDefaultRouteActionFaultInjectionPolicyAbortHttpStatus(original["http_status"], d, config)
+	if err != nil {
+		return nil, err
+	} else if val := reflect.ValueOf(transformedHttpStatus); val.IsValid() && !isEmptyValue(val) {
+		transformed["httpStatus"] = transformedHttpStatus
+	}
+
+	transformedPercentage, err := expandComputeRegionUrlMapDefaultRouteActionFaultInjectionPolicyAbortPercentage(original["percentage"], d, config)
+	if err != nil {
+		return nil, err
+	} else if val := reflect.ValueOf(transformedPercentage); val.IsValid() && !isEmptyValue(val) {
+		transformed["percentage"] = transformedPercentage
+	}
+
+	return transformed, nil
+}
+
+func expandComputeRegionUrlMapDefaultRouteActionFaultInjectionPolicyAbortHttpStatus(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) {
+	return v, nil
+}
+
+func expandComputeRegionUrlMapDefaultRouteActionFaultInjectionPolicyAbortPercentage(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) {
+	return v, nil
+}
+
 func expandComputeRegionUrlMapRegion(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) {
 	f, err := parseGlobalFieldValue("regions", v.(string), "project", d, config, true)
 	if err != nil {
diff --git a/converters/google/resources/iam_vertex_ai_featurestore_entitytype.go b/converters/google/resources/iam_vertex_ai_featurestore_entitytype.go
new file mode 100644
index 00000000..8c88b85b
--- /dev/null
+++ b/converters/google/resources/iam_vertex_ai_featurestore_entitytype.go
@@ -0,0 +1,182 @@
+// ----------------------------------------------------------------------------
+//
+//     ***     AUTO GENERATED CODE    ***    Type: MMv1     ***
+//
+// ----------------------------------------------------------------------------
+//
+//     This file is automatically generated by Magic Modules and manual
+//     changes will be clobbered when the file is regenerated.
+//
+//     Please read more about how to change this file in
+//     .github/CONTRIBUTING.md.
+//
+// ----------------------------------------------------------------------------
+
+package google
+
+import (
+	"fmt"
+
+	"github.com/hashicorp/errwrap"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+	"google.golang.org/api/cloudresourcemanager/v1"
+)
+
+var VertexAIFeaturestoreEntitytypeIamSchema = map[string]*schema.Schema{
+	"featurestore": {
+		Type:     schema.TypeString,
+		Required: true,
+		ForceNew: true,
+	},
+	"entitytype": {
+		Type:             schema.TypeString,
+		Required:         true,
+		ForceNew:         true,
+		DiffSuppressFunc: compareSelfLinkOrResourceName,
+	},
+}
+
+type VertexAIFeaturestoreEntitytypeIamUpdater struct {
+	featurestore string
+	entitytype   string
+	d            TerraformResourceData
+	Config       *Config
+}
+
+func VertexAIFeaturestoreEntitytypeIamUpdaterProducer(d TerraformResourceData, config *Config) (ResourceIamUpdater, error) {
+	values := make(map[string]string)
+
+	if v, ok := d.GetOk("featurestore"); ok {
+		values["featurestore"] = v.(string)
+	}
+
+	if v, ok := d.GetOk("entitytype"); ok {
+		values["entitytype"] = v.(string)
+	}
+
+	// We may have gotten either a long or short name, so attempt to parse long name if possible
+	m, err := getImportIdQualifiers([]string{"(?P<featurestore>.+)/entityTypes/(?P<entitytype>[^/]+)", "(?P<entitytype>[^/]+)"}, d, config, d.Get("entitytype").(string))
+	if err != nil {
+		return nil, err
+	}
+
+	for k, v := range m {
+		values[k] = v
+	}
+
+	u := &VertexAIFeaturestoreEntitytypeIamUpdater{
+		featurestore: values["featurestore"],
+		entitytype:   values["entitytype"],
+		d:            d,
+		Config:       config,
+	}
+
+	if err := d.Set("featurestore", u.featurestore); err != nil {
+		return nil, fmt.Errorf("Error setting featurestore: %s", err)
+	}
+	if err := d.Set("entitytype", u.GetResourceId()); err != nil {
+		return nil, fmt.Errorf("Error setting entitytype: %s", err)
+	}
+
+	return u, nil
+}
+
+func VertexAIFeaturestoreEntitytypeIdParseFunc(d *schema.ResourceData, config *Config) error {
+	values := make(map[string]string)
+
+	m, err := getImportIdQualifiers([]string{"(?P<featurestore>.+)/entityTypes/(?P<entitytype>[^/]+)", "(?P<entitytype>[^/]+)"}, d, config, d.Id())
+	if err != nil {
+		return err
+	}
+
+	for k, v := range m {
+		values[k] = v
+	}
+
+	u := &VertexAIFeaturestoreEntitytypeIamUpdater{
+		featurestore: values["featurestore"],
+		entitytype:   values["entitytype"],
+		d:            d,
+		Config:       config,
+	}
+	if err := d.Set("entitytype", u.GetResourceId()); err != nil {
+		return fmt.Errorf("Error setting entitytype: %s", err)
+	}
+	d.SetId(u.GetResourceId())
+	return nil
+}
+
+func (u *VertexAIFeaturestoreEntitytypeIamUpdater) GetResourceIamPolicy() (*cloudresourcemanager.Policy, error) {
+	url, err := u.qualifyFeaturestoreEntitytypeUrl("getIamPolicy")
+	if err != nil {
+		return nil, err
+	}
+
+	var obj map[string]interface{}
+
+	userAgent, err := generateUserAgentString(u.d, u.Config.userAgent)
+	if err != nil {
+		return nil, err
+	}
+
+	policy, err := sendRequest(u.Config, "POST", "", url, userAgent, obj)
+	if err != nil {
+		return nil, errwrap.Wrapf(fmt.Sprintf("Error retrieving IAM policy for %s: {{err}}", u.DescribeResource()), err)
+	}
+
+	out := &cloudresourcemanager.Policy{}
+	err = Convert(policy, out)
+	if err != nil {
+		return nil, errwrap.Wrapf("Cannot convert a policy to a resource manager policy: {{err}}", err)
+	}
+
+	return out, nil
+}
+
+func (u *VertexAIFeaturestoreEntitytypeIamUpdater) SetResourceIamPolicy(policy *cloudresourcemanager.Policy) error {
+	json, err := ConvertToMap(policy)
+	if err != nil {
+		return err
+	}
+
+	obj := make(map[string]interface{})
+	obj["policy"] = json
+
+	url, err := u.qualifyFeaturestoreEntitytypeUrl("setIamPolicy")
+	if err != nil {
+		return err
+	}
+
+	userAgent, err := generateUserAgentString(u.d, u.Config.userAgent)
+	if err != nil {
+		return err
+	}
+
+	_, err = sendRequestWithTimeout(u.Config, "POST", "", url, userAgent, obj, u.d.Timeout(schema.TimeoutCreate))
+	if err != nil {
+		return errwrap.Wrapf(fmt.Sprintf("Error setting IAM policy for %s: {{err}}", u.DescribeResource()), err)
+	}
+
+	return nil
+}
+
+func (u *VertexAIFeaturestoreEntitytypeIamUpdater) qualifyFeaturestoreEntitytypeUrl(methodIdentifier string) (string, error) {
+	urlTemplate := fmt.Sprintf("{{VertexAIBasePath}}%s:%s", fmt.Sprintf("%s/entityTypes/%s", u.featurestore, u.entitytype), methodIdentifier)
+	url, err := replaceVars(u.d, u.Config, urlTemplate)
+	if err != nil {
+		return "", err
+	}
+	return url, nil
+}
+
+func (u *VertexAIFeaturestoreEntitytypeIamUpdater) GetResourceId() string {
+	return fmt.Sprintf("%s/entityTypes/%s", u.featurestore, u.entitytype)
+}
+
+func (u *VertexAIFeaturestoreEntitytypeIamUpdater) GetMutexKey() string {
+	return fmt.Sprintf("iam-vertexai-featurestoreentitytype-%s", u.GetResourceId())
+}
+
+func (u *VertexAIFeaturestoreEntitytypeIamUpdater) DescribeResource() string {
+	return fmt.Sprintf("vertexai featurestoreentitytype %q", u.GetResourceId())
+}
diff --git a/converters/google/resources/metadata.go b/converters/google/resources/metadata.go
index dceb18e1..24c0f661 100644
--- a/converters/google/resources/metadata.go
+++ b/converters/google/resources/metadata.go
@@ -142,8 +142,11 @@ func resourceInstanceMetadata(d TerraformResourceData) (*compute.Metadata, error
 	m := &compute.Metadata{}
 	mdMap := d.Get("metadata").(map[string]interface{})
 	if v, ok := d.GetOk("metadata_startup_script"); ok && v.(string) != "" {
-		if _, ok := mdMap["startup-script"]; ok {
-			return nil, errors.New("Cannot provide both metadata_startup_script and metadata.startup-script.")
+		if w, ok := mdMap["startup-script"]; ok {
+			// metadata.startup-script could be from metadata_startup_script in the first place
+			if v != w {
+				return nil, errors.New("Cannot provide both metadata_startup_script and metadata.startup-script.")
+			}
 		}
 		mdMap["startup-script"] = v
 	}
diff --git a/converters/google/resources/resource_converters.go b/converters/google/resources/resource_converters.go
index 264bc1b4..ac419de2 100644
--- a/converters/google/resources/resource_converters.go
+++ b/converters/google/resources/resource_converters.go
@@ -202,6 +202,9 @@ func ResourceConverters() map[string][]ResourceConverter {
 		"google_vertex_ai_featurestore_iam_policy":                {resourceConverterVertexAIFeaturestoreIamPolicy()},
 		"google_vertex_ai_featurestore_iam_binding":               {resourceConverterVertexAIFeaturestoreIamBinding()},
 		"google_vertex_ai_featurestore_iam_member":                {resourceConverterVertexAIFeaturestoreIamMember()},
+		"google_vertex_ai_featurestore_entitytype_iam_policy":     {resourceConverterVertexAIFeaturestoreEntitytypeIamPolicy()},
+		"google_vertex_ai_featurestore_entitytype_iam_binding":    {resourceConverterVertexAIFeaturestoreEntitytypeIamBinding()},
+		"google_vertex_ai_featurestore_entitytype_iam_member":     {resourceConverterVertexAIFeaturestoreEntitytypeIamMember()},
 		"google_project": {
 			resourceConverterProject(),
 			resourceConverterProjectBillingInfo(),
diff --git a/converters/google/resources/storage_bucket.go b/converters/google/resources/storage_bucket.go
index ddb4d829..ad6096e6 100644
--- a/converters/google/resources/storage_bucket.go
+++ b/converters/google/resources/storage_bucket.go
@@ -149,7 +149,7 @@ func expandBucketEncryption(configured interface{}) *storage.BucketEncryption {
 
 func expandBucketLogging(configured interface{}) *storage.BucketLogging {
 	loggings := configured.([]interface{})
-	if len(loggings) == 0 {
+	if len(loggings) == 0 || loggings[0] == nil {
 		return nil
 	}
 
diff --git a/converters/google/resources/utils.go b/converters/google/resources/utils.go
index 0005cec2..c04c93bc 100644
--- a/converters/google/resources/utils.go
+++ b/converters/google/resources/utils.go
@@ -519,6 +519,9 @@ func GetCurrentUserEmail(config *Config, userAgent string) (string, error) {
 	if err != nil {
 		return "", fmt.Errorf("error retrieving userinfo for your provider credentials. have you enabled the 'https://www.googleapis.com/auth/userinfo.email' scope? error: %s", err)
 	}
+	if res["email"] == nil {
+		return "", fmt.Errorf("error retrieving email from userinfo. email was nil in the response.")
+	}
 	return res["email"].(string), nil
 }
 
diff --git a/converters/google/resources/vertex_ai_featurestore_entitytype_iam.go b/converters/google/resources/vertex_ai_featurestore_entitytype_iam.go
new file mode 100644
index 00000000..4327a27c
--- /dev/null
+++ b/converters/google/resources/vertex_ai_featurestore_entitytype_iam.go
@@ -0,0 +1,123 @@
+// ----------------------------------------------------------------------------
+//
+//     ***     AUTO GENERATED CODE    ***    Type: MMv1     ***
+//
+// ----------------------------------------------------------------------------
+//
+//     This file is automatically generated by Magic Modules and manual
+//     changes will be clobbered when the file is regenerated.
+//
+//     Please read more about how to change this file in
+//     .github/CONTRIBUTING.md.
+//
+// ----------------------------------------------------------------------------
+
+package google
+
+import "fmt"
+
+// Provide a separate asset type constant so we don't have to worry about name conflicts between IAM and non-IAM converter files
+const VertexAIFeaturestoreEntitytypeIAMAssetType string = "{{region}}-aiplatform.googleapis.com/FeaturestoreEntitytype"
+
+func resourceConverterVertexAIFeaturestoreEntitytypeIamPolicy() ResourceConverter {
+	return ResourceConverter{
+		AssetType:         VertexAIFeaturestoreEntitytypeIAMAssetType,
+		Convert:           GetVertexAIFeaturestoreEntitytypeIamPolicyCaiObject,
+		MergeCreateUpdate: MergeVertexAIFeaturestoreEntitytypeIamPolicy,
+	}
+}
+
+func resourceConverterVertexAIFeaturestoreEntitytypeIamBinding() ResourceConverter {
+	return ResourceConverter{
+		AssetType:         VertexAIFeaturestoreEntitytypeIAMAssetType,
+		Convert:           GetVertexAIFeaturestoreEntitytypeIamBindingCaiObject,
+		FetchFullResource: FetchVertexAIFeaturestoreEntitytypeIamPolicy,
+		MergeCreateUpdate: MergeVertexAIFeaturestoreEntitytypeIamBinding,
+		MergeDelete:       MergeVertexAIFeaturestoreEntitytypeIamBindingDelete,
+	}
+}
+
+func resourceConverterVertexAIFeaturestoreEntitytypeIamMember() ResourceConverter {
+	return ResourceConverter{
+		AssetType:         VertexAIFeaturestoreEntitytypeIAMAssetType,
+		Convert:           GetVertexAIFeaturestoreEntitytypeIamMemberCaiObject,
+		FetchFullResource: FetchVertexAIFeaturestoreEntitytypeIamPolicy,
+		MergeCreateUpdate: MergeVertexAIFeaturestoreEntitytypeIamMember,
+		MergeDelete:       MergeVertexAIFeaturestoreEntitytypeIamMemberDelete,
+	}
+}
+
+func GetVertexAIFeaturestoreEntitytypeIamPolicyCaiObject(d TerraformResourceData, config *Config) ([]Asset, error) {
+	return newVertexAIFeaturestoreEntitytypeIamAsset(d, config, expandIamPolicyBindings)
+}
+
+func GetVertexAIFeaturestoreEntitytypeIamBindingCaiObject(d TerraformResourceData, config *Config) ([]Asset, error) {
+	return newVertexAIFeaturestoreEntitytypeIamAsset(d, config, expandIamRoleBindings)
+}
+
+func GetVertexAIFeaturestoreEntitytypeIamMemberCaiObject(d TerraformResourceData, config *Config) ([]Asset, error) {
+	return newVertexAIFeaturestoreEntitytypeIamAsset(d, config, expandIamMemberBindings)
+}
+
+func MergeVertexAIFeaturestoreEntitytypeIamPolicy(existing, incoming Asset) Asset {
+	existing.IAMPolicy = incoming.IAMPolicy
+	return existing
+}
+
+func MergeVertexAIFeaturestoreEntitytypeIamBinding(existing, incoming Asset) Asset {
+	return mergeIamAssets(existing, incoming, mergeAuthoritativeBindings)
+}
+
+func MergeVertexAIFeaturestoreEntitytypeIamBindingDelete(existing, incoming Asset) Asset {
+	return mergeDeleteIamAssets(existing, incoming, mergeDeleteAuthoritativeBindings)
+}
+
+func MergeVertexAIFeaturestoreEntitytypeIamMember(existing, incoming Asset) Asset {
+	return mergeIamAssets(existing, incoming, mergeAdditiveBindings)
+}
+
+func MergeVertexAIFeaturestoreEntitytypeIamMemberDelete(existing, incoming Asset) Asset {
+	return mergeDeleteIamAssets(existing, incoming, mergeDeleteAdditiveBindings)
+}
+
+func newVertexAIFeaturestoreEntitytypeIamAsset(
+	d TerraformResourceData,
+	config *Config,
+	expandBindings func(d TerraformResourceData) ([]IAMBinding, error),
+) ([]Asset, error) {
+	bindings, err := expandBindings(d)
+	if err != nil {
+		return []Asset{}, fmt.Errorf("expanding bindings: %v", err)
+	}
+
+	name, err := assetName(d, config, "//{{region}}-aiplatform.googleapis.com/{{featurestore}}/entityTypes/{{entitytype}}")
+	if err != nil {
+		return []Asset{}, err
+	}
+
+	return []Asset{{
+		Name: name,
+		Type: VertexAIFeaturestoreEntitytypeIAMAssetType,
+		IAMPolicy: &IAMPolicy{
+			Bindings: bindings,
+		},
+	}}, nil
+}
+
+func FetchVertexAIFeaturestoreEntitytypeIamPolicy(d TerraformResourceData, config *Config) (Asset, error) {
+	// Check if the identity field returns a value
+	if _, ok := d.GetOk("featurestore"); !ok {
+		return Asset{}, ErrEmptyIdentityField
+	}
+	if _, ok := d.GetOk("entitytype"); !ok {
+		return Asset{}, ErrEmptyIdentityField
+	}
+
+	return fetchIamPolicy(
+		VertexAIFeaturestoreEntitytypeIamUpdaterProducer,
+		d,
+		config,
+		"//{{region}}-aiplatform.googleapis.com/{{featurestore}}/entityTypes/{{entitytype}}",
+		VertexAIFeaturestoreEntitytypeIAMAssetType,
+	)
+}
diff --git a/converters/google/resources/vertexai_tensorboard.go b/converters/google/resources/vertexai_tensorboard.go
new file mode 100644
index 00000000..d6753429
--- /dev/null
+++ b/converters/google/resources/vertexai_tensorboard.go
@@ -0,0 +1,119 @@
+// ----------------------------------------------------------------------------
+//
+//     ***     AUTO GENERATED CODE    ***    Type: MMv1     ***
+//
+// ----------------------------------------------------------------------------
+//
+//     This file is automatically generated by Magic Modules and manual
+//     changes will be clobbered when the file is regenerated.
+//
+//     Please read more about how to change this file in
+//     .github/CONTRIBUTING.md.
+//
+// ----------------------------------------------------------------------------
+
+package google
+
+import "reflect"
+
+const VertexAITensorboardAssetType string = "{{region}}-aiplatform.googleapis.com/Tensorboard"
+
+func resourceConverterVertexAITensorboard() ResourceConverter {
+	return ResourceConverter{
+		AssetType: VertexAITensorboardAssetType,
+		Convert:   GetVertexAITensorboardCaiObject,
+	}
+}
+
+func GetVertexAITensorboardCaiObject(d TerraformResourceData, config *Config) ([]Asset, error) {
+	name, err := assetName(d, config, "//{{region}}-aiplatform.googleapis.com/{{name}}")
+	if err != nil {
+		return []Asset{}, err
+	}
+	if obj, err := GetVertexAITensorboardApiObject(d, config); err == nil {
+		return []Asset{{
+			Name: name,
+			Type: VertexAITensorboardAssetType,
+			Resource: &AssetResource{
+				Version:              "v1",
+				DiscoveryDocumentURI: "https://www.googleapis.com/discovery/v1/apis/{{region}}-aiplatform/v1/rest",
+				DiscoveryName:        "Tensorboard",
+				Data:                 obj,
+			},
+		}}, nil
+	} else {
+		return []Asset{}, err
+	}
+}
+
+func GetVertexAITensorboardApiObject(d TerraformResourceData, config *Config) (map[string]interface{}, error) {
+	obj := make(map[string]interface{})
+	displayNameProp, err := expandVertexAITensorboardDisplayName(d.Get("display_name"), d, config)
+	if err != nil {
+		return nil, err
+	} else if v, ok := d.GetOkExists("display_name"); !isEmptyValue(reflect.ValueOf(displayNameProp)) && (ok || !reflect.DeepEqual(v, displayNameProp)) {
+		obj["displayName"] = displayNameProp
+	}
+	descriptionProp, err := expandVertexAITensorboardDescription(d.Get("description"), d, config)
+	if err != nil {
+		return nil, err
+	} else if v, ok := d.GetOkExists("description"); !isEmptyValue(reflect.ValueOf(descriptionProp)) && (ok || !reflect.DeepEqual(v, descriptionProp)) {
+		obj["description"] = descriptionProp
+	}
+	encryptionSpecProp, err := expandVertexAITensorboardEncryptionSpec(d.Get("encryption_spec"), d, config)
+	if err != nil {
+		return nil, err
+	} else if v, ok := d.GetOkExists("encryption_spec"); !isEmptyValue(reflect.ValueOf(encryptionSpecProp)) && (ok || !reflect.DeepEqual(v, encryptionSpecProp)) {
+		obj["encryptionSpec"] = encryptionSpecProp
+	}
+	labelsProp, err := expandVertexAITensorboardLabels(d.Get("labels"), d, config)
+	if err != nil {
+		return nil, err
+	} else if v, ok := d.GetOkExists("labels"); !isEmptyValue(reflect.ValueOf(labelsProp)) && (ok || !reflect.DeepEqual(v, labelsProp)) {
+		obj["labels"] = labelsProp
+	}
+
+	return obj, nil
+}
+
+func expandVertexAITensorboardDisplayName(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) {
+	return v, nil
+}
+
+func expandVertexAITensorboardDescription(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) {
+	return v, nil
+}
+
+func expandVertexAITensorboardEncryptionSpec(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) {
+	l := v.([]interface{})
+	if len(l) == 0 || l[0] == nil {
+		return nil, nil
+	}
+	raw := l[0]
+	original := raw.(map[string]interface{})
+	transformed := make(map[string]interface{})
+
+	transformedKmsKeyName, err := expandVertexAITensorboardEncryptionSpecKmsKeyName(original["kms_key_name"], d, config)
+	if err != nil {
+		return nil, err
+	} else if val := reflect.ValueOf(transformedKmsKeyName); val.IsValid() && !isEmptyValue(val) {
+		transformed["kmsKeyName"] = transformedKmsKeyName
+	}
+
+	return transformed, nil
+}
+
+func expandVertexAITensorboardEncryptionSpecKmsKeyName(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) {
+	return v, nil
+}
+
+func expandVertexAITensorboardLabels(v interface{}, d TerraformResourceData, config *Config) (map[string]string, error) {
+	if v == nil {
+		return map[string]string{}, nil
+	}
+	m := make(map[string]string)
+	for k, val := range v.(map[string]interface{}) {
+		m[k] = val.(string)
+	}
+	return m, nil
+}
diff --git a/go.mod b/go.mod
index e1e92b39..e929213b 100644
--- a/go.mod
+++ b/go.mod
@@ -26,7 +26,7 @@ require (
 	github.com/hashicorp/go-cleanhttp v0.5.2
 	github.com/hashicorp/terraform-json v0.14.0
 	github.com/hashicorp/terraform-plugin-sdk/v2 v2.18.0
-	github.com/hashicorp/terraform-provider-google v1.20.1-0.20221116173652-5edb89e58065
+	github.com/hashicorp/terraform-provider-google v1.20.1-0.20221128224349-6acf8368b74e
 	github.com/mitchellh/go-homedir v1.1.0
 	github.com/pkg/errors v0.9.1
 	github.com/sirupsen/logrus v1.8.1
diff --git a/go.sum b/go.sum
index 9fb52479..779f23f7 100644
--- a/go.sum
+++ b/go.sum
@@ -658,8 +658,8 @@ github.com/hashicorp/terraform-plugin-log v0.4.1 h1:xpbmVhvuU3mgHzLetOmx9pkOL2rm
 github.com/hashicorp/terraform-plugin-log v0.4.1/go.mod h1:p4R1jWBXRTvL4odmEkFfDdhUjHf9zcs/BCoNHAc7IK4=
 github.com/hashicorp/terraform-plugin-sdk/v2 v2.18.0 h1:/cdI5di5XA+N80gXzXF4YcHq36DprBskubk6Z8i26ZQ=
 github.com/hashicorp/terraform-plugin-sdk/v2 v2.18.0/go.mod h1:L3SHkD/Q8zPVgXviQmpVwy9nKwpXXZscVIpVEnQ/T50=
-github.com/hashicorp/terraform-provider-google v1.20.1-0.20221116173652-5edb89e58065 h1:PFK1p0RC+hKZc6wM8HQULI614yJH2JrwW36iPiYcreg=
-github.com/hashicorp/terraform-provider-google v1.20.1-0.20221116173652-5edb89e58065/go.mod h1:J1q/lz9dSnciHwY+rjQRs0r+M3pe0+0G887jFq8COvM=
+github.com/hashicorp/terraform-provider-google v1.20.1-0.20221128224349-6acf8368b74e h1:lYPGOKk38VVcAai2LlfIhnTm0Tb1xDcEGSqh72ryRL8=
+github.com/hashicorp/terraform-provider-google v1.20.1-0.20221128224349-6acf8368b74e/go.mod h1:J1q/lz9dSnciHwY+rjQRs0r+M3pe0+0G887jFq8COvM=
 github.com/hashicorp/terraform-registry-address v0.0.0-20220623143253-7d51757b572c h1:D8aRO6+mTqHfLsK/BC3j5OAoogv1WLRWzY1AaTo3rBg=
 github.com/hashicorp/terraform-registry-address v0.0.0-20220623143253-7d51757b572c/go.mod h1:Wn3Na71knbXc1G8Lh+yu/dQWWJeFQEpDeJMtWMtlmNI=
 github.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734 h1:HKLsbzeOsfXmKNpr3GiT18XAblV0BjCbzL8KQAMZGa0=


Test patch:
diff --git a/ancestrymanager/ancestrymanager_test.go b/ancestrymanager/ancestrymanager_test.go
index e971ad9d..713ac4e8 100644
--- a/ancestrymanager/ancestrymanager_test.go
+++ b/ancestrymanager/ancestrymanager_test.go
@@ -18,6 +18,7 @@ import (
 	crmv1 "google.golang.org/api/cloudresourcemanager/v1"
 	crmv3 "google.golang.org/api/cloudresourcemanager/v3"
 	"google.golang.org/api/option"
+	"google.golang.org/api/storage/v1"
 )
 
 func TestGetAncestors(t *testing.T) {
@@ -1207,3 +1208,113 @@ func TestUnknownProject(t *testing.T) {
 		})
 	}
 }
+
+func TestGetProjectFromResource(t *testing.T) {
+	p := provider.Provider()
+	cases := []struct {
+		name   string
+		asset  *resources.Asset
+		config *resources.Config
+		d      resources.TerraformResourceData
+		resp   *storage.Bucket
+		want   string
+	}{
+		{
+			name: "bucket - from cai resource",
+			config: &resources.Config{
+				Project: "test-project",
+			},
+			d: tfdata.NewFakeResourceData(
+				"google_storage_bucket_iam_member",
+				p.ResourcesMap["google_storage_bucket_iam_member"].Schema,
+				map[string]interface{}{
+					"bucket": "bucket-name",
+				},
+			),
+			asset: &resources.Asset{
+				Type: "storage.googleapis.com/Bucket",
+				Resource: &resources.AssetResource{
+					Data: map[string]interface{}{
+						"project": "resource-project",
+					},
+				},
+			},
+			resp: &storage.Bucket{
+				ProjectNumber: 123,
+			},
+			want: "resource-project",
+		},
+		{
+			name:   "bucket - from storage API",
+			config: &resources.Config{Project: "test-project"},
+			d: tfdata.NewFakeResourceData(
+				"google_storage_bucket_iam_member",
+				p.ResourcesMap["google_storage_bucket_iam_member"].Schema,
+				map[string]interface{}{
+					"bucket": "bucket-name",
+				},
+			),
+			asset: &resources.Asset{
+				Type: "storage.googleapis.com/Bucket",
+			},
+			resp: &storage.Bucket{
+				ProjectNumber: 123,
+			},
+			want: "123",
+		},
+		{
+			name:   "bucket - from provider config",
+			config: &resources.Config{Project: "test-project"},
+			d: tfdata.NewFakeResourceData(
+				"google_storage_bucket_iam_member",
+				p.ResourcesMap["google_storage_bucket_iam_member"].Schema,
+				map[string]interface{}{
+					"bucket": "bucket-name",
+				},
+			),
+			asset: &resources.Asset{
+				Type: "storage.googleapis.com/Bucket",
+			},
+			resp: nil,
+			want: "test-project",
+		},
+	}
+
+	for _, c := range cases {
+		t.Run(c.name, func(t *testing.T) {
+			ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+				if c.resp == nil {
+					w.WriteHeader(http.StatusForbidden)
+					w.Write([]byte("no response"))
+					return
+				}
+
+				payload, err := c.resp.MarshalJSON()
+				if err != nil {
+					w.WriteHeader(http.StatusInternalServerError)
+					w.Write([]byte(fmt.Sprintf("failed to MarshalJSON: %s", err)))
+					return
+				}
+				w.Write(payload)
+			}))
+			defer ts.Close()
+
+			mockClient, err := storage.NewService(context.Background(), option.WithEndpoint(ts.URL), option.WithoutAuthentication())
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			ancestryManager := &manager{
+				errorLogger:   zap.NewExample(),
+				storageClient: mockClient,
+			}
+			got, err := ancestryManager.getProjectFromResource(c.d, c.config, c.asset)
+			if err != nil {
+				t.Fatalf("getProjectFromResource() = %s, want = nil", err)
+			}
+			if got != c.want {
+				t.Fatalf("getProjectFromResource() = %s, want = %s", got, c.want)
+			}
+		})
+	}
+}