Repository: GoogleCloudPlatform/terraform-validator
Pull request #606 (2022-03-24 19:56:38)

Refactor ancestry manager to use cloud resource manager API to fetch folder ancestry
b/211908063 to support folder and organization as the ancestry. folder and organization are not bound to projects in the context. 

The test files will be overwritten by magic-module, so the corresponding PR in magic module is 
in https://github.com/GoogleCloudPlatform/magic-modules/pull/6033. 

- for cai type = Project, if a project does not exist(presuming a new project), it gets 403 from cloud resource manager. In that case, ancestry manager does not return an error, but returns the project itself as the only ancestor. 
- Tests
  - the integration test has to specify the real TEST_ORG_ID, TEST_ANCESTRY, TEST_FOLDER_ID to get it work, since in the online mode, it has no cache, and it tries to fetch a folder's parent
  - have to adjust example_project_update to use a hard coded org id in order to match the violation definition. 


iyabchen: To get the integration test passing, need to set TEST_ORG_ID=<NUMBER1>, TEST_FOLDER_ID=<NUMBER2>, TEST_ANCESTRY=organization/<NUMBER1>/folder/<NUMBER2> to have the real values. 
melinath: @iyabchen The env vars should all be set up on the main branch now!

Patch:
diff --git a/ancestrymanager/ancestrymanager.go b/ancestrymanager/ancestrymanager.go
index 5d8dc8f8..0ec25ba6 100644
--- a/ancestrymanager/ancestrymanager.go
+++ b/ancestrymanager/ancestrymanager.go
@@ -1,189 +1,281 @@
-// Package ancestrymanager provides an interface to query the ancestry information for a project.
+// Package ancestrymanager provides an interface to query the ancestry information for a resource.
 package ancestrymanager
 
 import (
 	"fmt"
 	"strings"
 
-	"google.golang.org/api/cloudresourcemanager/v1"
+	"google.golang.org/api/cloudresourcemanager/v3"
 
 	resources "github.com/GoogleCloudPlatform/terraform-validator/converters/google/resources"
+
 	"go.uber.org/zap"
 )
 
-// AncestryManager is the interface that wraps the GetAncestry method.
+// AncestryManager is the interface that fetch ancestors for a resource.
 type AncestryManager interface {
-	// GetAncestry takes a project name to return an ancestry path
-	GetAncestry(project string) (string, error)
-	// GetAncestry takes a project name and resource data to return an ancestry path
-	GetAncestryWithResource(project string, tfData resources.TerraformResourceData, cai resources.Asset) (string, error)
+	// Ancestors returns a list of ancestors.
+	Ancestors(config *resources.Config, tfData resources.TerraformResourceData, cai *resources.Asset) ([]string, string, error)
 }
 
-// resourceAncestryManager provides common methods for retrieving ancestry from resources
-type resourceAncestryManager struct {
+type manager struct {
+	// The logger.
 	errorLogger *zap.Logger
+	// GCP resource manager service. If this field is nil, online lookups will
+	// be disabled.
+	resourceManager *cloudresourcemanager.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.
+	ancestorCache map[string][]string
 }
 
-func (m *resourceAncestryManager) getFolderAncestry(folderID string) ([]*cloudresourcemanager.Ancestor, error) {
-	// TODO(morgantep): Incorporate folders.GetAncestry from v2alpha1 API
-	m.errorLogger.Info(fmt.Sprintf("Retrieving ancestry for folder: %s", folderID))
-
-	return []*cloudresourcemanager.Ancestor{
-		&cloudresourcemanager.Ancestor{
-			ResourceId: &cloudresourcemanager.ResourceId{
-				Type: "folder",
-				Id:   folderID,
-			},
-		},
-		&cloudresourcemanager.Ancestor{
-			ResourceId: &cloudresourcemanager.ResourceId{
-				Type: "organization",
-				Id:   "unknown",
-			},
-		},
-	}, nil
+// New returns AncestryManager that can be used to fetch ancestry information.
+// Entries takes `projects/<number>` or `folders/<id>` as key and ancestry path
+// as value to the offline cache. If the key is not prefix with `projects/` or
+// `folders/`, it will be considered as a project. If no resourceManager is
+// provided, API requests for ancestry will be disabled.
+func New(resourceManager *cloudresourcemanager.Service, entries map[string]string, errorLogger *zap.Logger) (AncestryManager, error) {
+	return newManager(resourceManager, entries, errorLogger)
 }
 
-func (m *resourceAncestryManager) getAncestryFromResource(tfData resources.TerraformResourceData, cai resources.Asset) ([]*cloudresourcemanager.Ancestor, bool) {
-	m.errorLogger.Info(fmt.Sprintf("Retrieving ancestry from resource (type=%s)", cai.Type))
+func newManager(resourceManager *cloudresourcemanager.Service, entries map[string]string, errorLogger *zap.Logger) (*manager, error) {
+	am := &manager{
+		ancestorCache:   map[string][]string{},
+		errorLogger:     errorLogger,
+		resourceManager: resourceManager,
+	}
+	err := am.initAncestryCache(entries)
+	if err != nil {
+		return nil, err
+	}
+	return am, nil
+}
 
-	switch cai.Type {
-	case "cloudresourcemanager.googleapis.com/Project", "cloudbilling.googleapis.com/ProjectBillingInfo":
-		// Prefer project number to project id if available;
-		// CAI exports use project number.
-		projectID, ok := tfData.GetOk("number")
-		if !ok {
-			projectID, ok = tfData.GetOk("project_id")
-			if !ok || projectID == "" {
-				return nil, false
+func (m *manager) initAncestryCache(entries map[string]string) error {
+	for item, ancestry := range entries {
+		if item != "" && ancestry != "" {
+			ancestors, err := parseAncestryPath(ancestry)
+			if err != nil {
+				continue
 			}
-		}
-
-		ancestry := []*cloudresourcemanager.Ancestor{
-			&cloudresourcemanager.Ancestor{
-				ResourceId: &cloudresourcemanager.ResourceId{
-					Type: "project",
-					Id:   projectID.(string),
-				},
-			},
-		}
-
-		orgID, ok := tfData.GetOk("org_id")
-		if ok && orgID != "" {
-			s := strings.Split(orgID.(string), "/")
-			return append(ancestry, &cloudresourcemanager.Ancestor{
-				ResourceId: &cloudresourcemanager.ResourceId{
-					Type: "organization",
-					Id:   s[len(s)-1],
-				},
-			}), true
-		}
-
-		folderID, ok := tfData.GetOk("folder_id")
-		if ok && folderID != "" {
-			folderAncestry, err := m.getFolderAncestry(folderID.(string))
+			key, err := parseAncestryKey(item)
 			if err != nil {
-				m.errorLogger.Error(fmt.Sprintf("Failed to retrieve folder ancestry: %s", err))
-				return nil, false
+				return err
 			}
-			return append(ancestry, folderAncestry...), true
+			// ancestry path should include the item itself
+			if ancestors[0] != key {
+				ancestors = append([]string{key}, ancestors...)
+			}
+			m.store(key, ancestors)
 		}
-		return nil, false
-	default:
-		m.errorLogger.Info(fmt.Sprintf("Resource of type %s does not include sufficient data for ancestry retrieval", cai.Type))
-		return nil, false
 	}
+	return nil
 }
 
-type manager struct {
-	resourceAncestryManager
-	// GCP resource manager service. If this field is nil, online lookups will be disabled.
-	resourceManager *cloudresourcemanager.Service
-	// Cache to prevent multiple network calls for looking up the same project's ancestry
-	// map[project]ancestryPath
-	ancestryCache map[string]string
+func parseAncestryKey(val string) (string, error) {
+	key := normalizeAncestry(val)
+	ix := strings.LastIndex(key, "/")
+	if ix == -1 {
+		// If not containing /, then treat it as a project.
+		return fmt.Sprintf("projects/%s", key), nil
+	} else {
+		k := key[:ix]
+		if k == "projects" || k == "folders" || k == "organizations" {
+			return key, nil
+		}
+		return "", fmt.Errorf("key with can only start with projects/, folders/, or organizations/")
+	}
 }
 
-// GetAncestry uses the resource manager API to get ancestry paths for
-// projects. It implements a cache because many resources share the same
-// project.
-func (m *manager) GetAncestry(project string) (string, error) {
-	if path, ok := m.ancestryCache[project]; ok {
-		return path, nil
+// Ancestors uses the resource manager API to get ancestors for resource.
+// It implements a cache because many resources share the same ancestors.
+func (m *manager) Ancestors(config *resources.Config, tfData resources.TerraformResourceData, cai *resources.Asset) ([]string, string, error) {
+	results, err := m.fetchAncestors(config, tfData, cai)
+	if err != nil {
+		return nil, "", err
 	}
 
-	if m.resourceManager == nil {
-		return "", fmt.Errorf("resourceManager required to fetch project ancestry from the API")
-	}
-	ancestry, err := m.resourceManager.Projects.GetAncestry(project, &cloudresourcemanager.GetAncestryRequest{}).Do()
+	parent, err := assetParent(cai, results)
 	if err != nil {
-		return "", err
+		return nil, "", err
 	}
-	path := ancestryPath(ancestry.Ancestor)
-	m.store(project, path)
-	return path, nil
+	return results, parent, nil
 }
 
-func (m *manager) store(project, ancestry string) {
-	if project != "" && ancestry != "" {
-		m.ancestryCache[project] = ancestry
+// fetchAncestors uses the resource manager API to get ancestors for resource.
+// It implements a cache because many resources share the same ancestors.
+func (m *manager) fetchAncestors(config *resources.Config, tfData resources.TerraformResourceData, cai *resources.Asset) ([]string, error) {
+	if cai == nil {
+		return nil, fmt.Errorf("CAI asset is nil")
 	}
-}
+	m.errorLogger.Info(fmt.Sprintf("Retrieving ancestry from resource (type=%s)", cai.Type))
+	key := ""
+	orgKey := ""
+	folderKey := ""
+
+	orgID, orgOK := getOrganizationFromResource(tfData)
+	if orgOK {
+		orgKey = orgID
+		if !strings.HasPrefix(orgKey, "organizations/") {
+			orgKey = fmt.Sprintf("organizations/%s", orgKey)
+		}
+	}
+	folderID, folderOK := getFolderFromResource(tfData)
+	if folderOK {
+		folderKey = folderID
+		if !strings.HasPrefix(folderKey, "folders/") {
+			folderKey = fmt.Sprintf("folders/%s", folderKey)
+		}
+	}
+	switch cai.Type {
+	case "cloudresourcemanager.googleapis.com/Folder":
+		if !folderOK {
+			return nil, fmt.Errorf("folder id not found in terraform data")
+		}
+		key = folderKey
+	case "cloudresourcemanager.googleapis.com/Organization":
+		if !orgOK {
+			return nil, fmt.Errorf("organization id not found in terraform data")
+		}
+		key = orgKey
+	case "iam.googleapis.com/Role":
+		// google_organization_iam_custom_role or google_project_iam_custom_role
+		if orgOK {
+			key = orgKey
+		} else {
+			project, err := getProjectFromResource(tfData, config, *cai, m.errorLogger)
+			if err != nil {
+				return nil, err
+			}
+			key = fmt.Sprintf("projects/%s", project)
+		}
+	case "cloudresourcemanager.googleapis.com/Project", "cloudbilling.googleapis.com/ProjectBillingInfo":
+		projectID, err := getProjectFromResource(tfData, config, *cai, m.errorLogger)
+		if err != nil {
+			return nil, err
+		}
 
-// GetAncestryWithResource first attempts to get Ancestry from the resource
-// If that fails, it falls back to the offline cache.
-func (m *manager) GetAncestryWithResource(project string, tfData resources.TerraformResourceData, cai resources.Asset) (string, error) {
-	ancestry, ok := m.getAncestryFromResource(tfData, cai)
-	if ok {
-		path := ancestryPath(ancestry)
-		m.store(project, path)
-		m.errorLogger.Info(fmt.Sprintf("Retrieved ancestry for %s: %s", project, path))
-		return path, nil
+		// Changing project_id forces a new project to be created.
+		// Changing folder_id or org_id forces project to be migrated.
+		// Hence ancestors should not be fetched from API in those scenarios.
+		if orgOK {
+			ancestors := []string{
+				fmt.Sprintf("projects/%s", projectID),
+				fmt.Sprintf("organizations/%s", orgID),
+			}
+			return ancestors, nil
+		}
+		if folderOK {
+			key = folderKey
+			ret, err := m.getAncestorsWithCache(key)
+			if err != nil {
+				return nil, err
+			}
+			ancestors := append([]string{fmt.Sprintf("projects/%s", projectID)}, ret...)
+			return ancestors, nil
+		}
+		key = fmt.Sprintf("projects/%s", projectID)
+		ancestors, err := m.getAncestorsWithCache(key)
+		if err != nil {
+			return nil, err
+		}
+		return ancestors, nil
+	default:
+		project, err := getProjectFromResource(tfData, config, *cai, m.errorLogger)
+		if err != nil {
+			return nil, err
+		}
+		key = fmt.Sprintf("projects/%s", project)
 	}
+	return m.getAncestorsWithCache(key)
+}
 
-	path, err := m.GetAncestry(project)
-	if err != nil {
-		return "", err
+func (m *manager) getAncestorsWithCache(key string) ([]string, error) {
+	var ancestors []string
+	cur := key
+	for cur != "" {
+		if cachedAncestors, ok := m.ancestorCache[cur]; ok {
+			ancestors = append(ancestors, cachedAncestors...)
+			break
+		}
+		if strings.HasPrefix(cur, "organizations/") {
+			ancestors = append(ancestors, cur)
+			break
+		}
+		if m.resourceManager == nil {
+			return nil, fmt.Errorf("resourceManager required to fetch ancestry for %s from the API", cur)
+		}
+		project, err := m.resourceManager.Projects.Get(cur).Do()
+		if err != nil {
+			return nil, err
+		}
+		ancestors = append(ancestors, project.Name)
+		cur = project.Parent
 	}
+	m.store(key, ancestors)
+	return ancestors, nil
+}
 
-	return path, nil
+func (m *manager) store(key string, ancestors []string) {
+	if key == "" || len(ancestors) == 0 {
+		return
+	}
+	if _, ok := m.ancestorCache[key]; !ok {
+		m.ancestorCache[key] = ancestors
+	}
+	// cache ancestors along the ancestry path
+	for i, ancestor := range ancestors {
+		if _, ok := m.ancestorCache[ancestor]; !ok {
+			m.ancestorCache[ancestor] = ancestors[i:]
+		}
+	}
 }
 
-// New returns AncestryManager that can be used to fetch ancestry information for a project.
-// entries takes project as key and ancestry as value to pre-warm the offline cache. If no
-// resourceManager is provided, API requests for ancestry will be disabled.
-func New(resourceManager *cloudresourcemanager.Service, entries map[string]string, errorLogger *zap.Logger) (AncestryManager, error) {
-	am := &manager{
-		ancestryCache: map[string]string{},
-		resourceAncestryManager: resourceAncestryManager{
-			errorLogger: errorLogger,
-		},
-		resourceManager: resourceManager,
+func parseAncestryPath(path string) ([]string, error) {
+	normStr := normalizeAncestry(path)
+	splits := strings.Split(normStr, "/")
+	if len(splits)%2 != 0 {
+		return nil, fmt.Errorf("unexpected format of ancestry path %s", path)
 	}
-	for project, ancestry := range entries {
-		if ancestry != "" {
-			ancestry = fmt.Sprintf("%s/project/%s", ancestry, project)
+	var ancestors []string
+	allowedPrefixes := map[string]bool{
+		"projects":      true,
+		"folders":       true,
+		"organizations": true,
+	}
+	for i := 0; i < len(splits); i = i + 2 {
+		if _, ok := allowedPrefixes[splits[i]]; !ok {
+			return nil, fmt.Errorf("invalid ancestry path %s with %s", path, splits[i])
 		}
-		am.store(project, ancestry)
+		ancestors = append(ancestors, fmt.Sprintf("%s/%s", splits[i], splits[i+1]))
 	}
-	return am, nil
+	// reverse the sequence
+	i, j := 0, len(ancestors)-1
+	for i < j {
+		ancestors[i], ancestors[j] = ancestors[j], ancestors[i]
+		i++
+		j--
+	}
+	return ancestors, nil
 }
 
-// ancestryPath composes a path containing organization/folder/project
-// (i.e. "organization/my-org/folder/my-folder/project/my-prj").
-func ancestryPath(as []*cloudresourcemanager.Ancestor) string {
-	var path []string
-	for i := len(as) - 1; i >= 0; i-- {
-		path = append(path, as[i].ResourceId.Type, as[i].ResourceId.Id)
+func normalizeAncestry(val string) string {
+	for _, r := range []struct {
+		old string
+		new string
+	}{
+		{"organization/", "organizations/"},
+		{"folder/", "folders/"},
+		{"project/", "projects/"},
+	} {
+		val = strings.ReplaceAll(val, r.old, r.new)
 	}
-	return strings.Join(path, "/")
+	return val
 }
 
 type NoOpAncestryManager struct{}
 
-func (m *NoOpAncestryManager) GetAncestry(project string) (string, error) {
-	return "", nil
-}
-func (m *NoOpAncestryManager) GetAncestryWithResource(project string, tfData resources.TerraformResourceData, cai resources.Asset) (string, error) {
-	return "", nil
+func (*NoOpAncestryManager) Ancestors(config *resources.Config, tfData resources.TerraformResourceData, cai *resources.Asset) ([]string, string, error) {
+	return nil, "", nil
 }
diff --git a/ancestrymanager/ancestryutil.go b/ancestrymanager/ancestryutil.go
new file mode 100644
index 00000000..9e5e0782
--- /dev/null
+++ b/ancestrymanager/ancestryutil.go
@@ -0,0 +1,136 @@
+package ancestrymanager
+
+import (
+	"fmt"
+	"strings"
+
+	resources "github.com/GoogleCloudPlatform/terraform-validator/converters/google/resources"
+
+	"go.uber.org/zap"
+)
+
+// assetParent derives a resource's parent from its ancestors.
+func assetParent(cai *resources.Asset, ancestors []string) (string, error) {
+	if cai == nil {
+		return "", fmt.Errorf("asset not provided")
+	}
+	switch cai.Type {
+	case "cloudresourcemanager.googleapis.com/Folder":
+		if len(ancestors) < 2 {
+			return "", fmt.Errorf("unexpected value for ancestors: %s", ancestors)
+		}
+		parent := ancestors[1]
+		if strings.HasPrefix(parent, "folders/") || strings.HasPrefix(parent, "organizations/") {
+			return fmt.Sprintf("//cloudresourcemanager.googleapis.com/%s", ancestors[1]), nil
+		}
+	case "cloudresourcemanager.googleapis.com/Organization":
+		return "", nil
+	case "cloudresourcemanager.googleapis.com/Project":
+		if len(ancestors) < 1 {
+			return "", fmt.Errorf("unexpected value for ancestors: %s", ancestors)
+		}
+		if len(ancestors) > 1 {
+			return fmt.Sprintf("//cloudresourcemanager.googleapis.com/%s", ancestors[1]), nil
+		}
+		// project creation/update
+		return fmt.Sprintf("//cloudresourcemanager.googleapis.com/%s", ancestors[0]), nil
+	default:
+		if len(ancestors) < 1 {
+			return "", fmt.Errorf("unexpected value for ancestors: %s", ancestors)
+		}
+		return fmt.Sprintf("//cloudresourcemanager.googleapis.com/%s", ancestors[0]), nil
+	}
+	return "", fmt.Errorf("unexpected value for ancestors: %v", ancestors)
+}
+
+// ConvertToAncestryPath composes a path containing organization/folder/project
+// (i.e. "organization/my-org/folder/my-folder/project/my-prj").
+func ConvertToAncestryPath(as []string) string {
+	var path []string
+	for i := len(as) - 1; i >= 0; i-- {
+		path = append(path, as[i])
+	}
+	str := strings.Join(path, "/")
+	return sanitizeAncestryPath(str)
+}
+
+func sanitizeAncestryPath(s string) string {
+	ret := s
+	// convert back to match existing ancestry path style.
+	for _, r := range []struct {
+		old string
+		new string
+	}{
+		{"organizations/", "organization/"},
+		{"folders/", "folder/"},
+		{"projects/", "project/"},
+	} {
+		ret = strings.ReplaceAll(ret, r.old, r.new)
+	}
+	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 != "" {
+		return res.(string), nil
+	}
+	if config.Project != "" {
+		return config.Project, nil
+	}
+	return "", fmt.Errorf("required field '%s' is not set, you may use --project=my-project to provide a default project to resolve the issue", projectSchemaField)
+}
+
+// getOrganizationFromResource reads org_id field from terraform data.
+func getOrganizationFromResource(tfData resources.TerraformResourceData) (string, bool) {
+	orgID, ok := tfData.GetOk("org_id")
+	if ok {
+		return orgID.(string), ok
+	}
+	return "", false
+}
+
+// getFolderFromResource reads folder_id or folder field from terraform data.
+func getFolderFromResource(tfData resources.TerraformResourceData) (string, bool) {
+	folderID, ok := tfData.GetOk("folder_id")
+	if ok {
+		return folderID.(string), ok
+	}
+	folderID, ok = tfData.GetOk("folder")
+	if ok {
+		return folderID.(string), ok
+	}
+	return "", false
+}
diff --git a/converters/google/convert.go b/converters/google/convert.go
index 333d4706..324d5a6e 100644
--- a/converters/google/convert.go
+++ b/converters/google/convert.go
@@ -21,14 +21,15 @@ import (
 	"strings"
 	"time"
 
+	"github.com/GoogleCloudPlatform/terraform-validator/ancestrymanager"
+	resources "github.com/GoogleCloudPlatform/terraform-validator/converters/google/resources"
+	"github.com/GoogleCloudPlatform/terraform-validator/tfdata"
+	"github.com/GoogleCloudPlatform/terraform-validator/tfplan"
+
 	tfjson "github.com/hashicorp/terraform-json"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
 	provider "github.com/hashicorp/terraform-provider-google/google"
 	"github.com/pkg/errors"
-
-	"github.com/GoogleCloudPlatform/terraform-validator/ancestrymanager"
-	resources "github.com/GoogleCloudPlatform/terraform-validator/converters/google/resources"
-	"github.com/GoogleCloudPlatform/terraform-validator/tfplan"
 	"go.uber.org/zap"
 )
 
@@ -223,7 +224,7 @@ func (c *Converter) AddResourceChanges(changes []*tfjson.ResourceChange) error {
 // can just happen without needing to be merged.
 func (c *Converter) addDelete(rc *tfjson.ResourceChange) error {
 	resource, _ := c.schema.ResourcesMap[rc.Type]
-	rd := NewFakeResourceData(
+	rd := tfdata.NewFakeResourceData(
 		rc.Type,
 		resource.Schema,
 		rc.Change.Before.(map[string]interface{}),
@@ -232,7 +233,7 @@ func (c *Converter) addDelete(rc *tfjson.ResourceChange) error {
 		if converter.FetchFullResource == nil || converter.MergeDelete == nil {
 			continue
 		}
-		convertedItems, err := converter.Convert(&rd, c.cfg)
+		convertedItems, err := converter.Convert(rd, c.cfg)
 
 		if err != nil {
 			if errors.Cause(err) == resources.ErrNoConversion {
@@ -248,7 +249,7 @@ func (c *Converter) addDelete(rc *tfjson.ResourceChange) error {
 			if existing, exists := c.assets[key]; exists {
 				existingConverterAsset = &existing.converterAsset
 			} else if !c.offline {
-				asset, err := converter.FetchFullResource(&rd, c.cfg)
+				asset, err := converter.FetchFullResource(rd, c.cfg)
 				if errors.Cause(err) == resources.ErrEmptyIdentityField {
 					c.errorLogger.Debug(fmt.Sprintf("%s: Unable to fetch and merge remote %s asset due to unset or (known after apply) identity fields on the TF resource.", rc.Address, converted.Type))
 					existingConverterAsset = nil
@@ -262,7 +263,7 @@ func (c *Converter) addDelete(rc *tfjson.ResourceChange) error {
 				}
 				if existingConverterAsset != nil {
 					converted = converter.MergeDelete(*existingConverterAsset, converted)
-					augmented, err := c.augmentAsset(&rd, c.cfg, converted)
+					augmented, err := c.augmentAsset(rd, c.cfg, converted)
 					if err != nil {
 						return err
 					}
@@ -280,14 +281,14 @@ func (c *Converter) addDelete(rc *tfjson.ResourceChange) error {
 // to be present.
 func (c *Converter) addCreateOrUpdateOrNoop(rc *tfjson.ResourceChange) error {
 	resource, _ := c.schema.ResourcesMap[rc.Type]
-	rd := NewFakeResourceData(
+	rd := tfdata.NewFakeResourceData(
 		rc.Type,
 		resource.Schema,
 		rc.Change.After.(map[string]interface{}),
 	)
 
 	for _, converter := range c.converters[rd.Kind()] {
-		convertedAssets, err := converter.Convert(&rd, c.cfg)
+		convertedAssets, err := converter.Convert(rd, c.cfg)
 		if err != nil {
 			if errors.Cause(err) == resources.ErrNoConversion {
 				continue
@@ -302,7 +303,7 @@ func (c *Converter) addCreateOrUpdateOrNoop(rc *tfjson.ResourceChange) error {
 			if existing, exists := c.assets[key]; exists {
 				existingConverterAsset = &existing.converterAsset
 			} else if converter.FetchFullResource != nil && !c.offline {
-				asset, err := converter.FetchFullResource(&rd, c.cfg)
+				asset, err := converter.FetchFullResource(rd, c.cfg)
 				if errors.Cause(err) == resources.ErrEmptyIdentityField {
 					c.errorLogger.Debug(fmt.Sprintf("%s: Unable to fetch and merge remote %s asset due to unset or (known after apply) identity fields on the TF resource.", rc.Address, converted.Type))
 					existingConverterAsset = nil
@@ -325,7 +326,7 @@ func (c *Converter) addCreateOrUpdateOrNoop(rc *tfjson.ResourceChange) error {
 				converted = converter.MergeCreateUpdate(*existingConverterAsset, converted)
 			}
 
-			augmented, err := c.augmentAsset(&rd, c.cfg, converted)
+			augmented, err := c.augmentAsset(rd, c.cfg, converted)
 			if err != nil {
 				return err
 			}
@@ -354,21 +355,19 @@ func (c *Converter) Assets() []Asset {
 
 // augmentAsset adds data to an asset that is not set by the conversion library.
 func (c *Converter) augmentAsset(tfData resources.TerraformResourceData, cfg *resources.Config, cai resources.Asset) (Asset, error) {
-	project, err := getProject(tfData, cfg, cai, c.errorLogger)
+	ancestors, parent, err := c.ancestryManager.Ancestors(cfg, tfData, &cai)
 	if err != nil {
-		return Asset{}, fmt.Errorf("getting project for %v: %w", cai.Name, err)
-	}
-	ancestry, err := c.ancestryManager.GetAncestryWithResource(project, tfData, cai)
-	if err != nil {
-		return Asset{}, fmt.Errorf("getting resource ancestry for project %v: %w", project, err)
+		return Asset{}, fmt.Errorf("getting resource ancestry or parent failed: %w", err)
 	}
+
+	// parent can be derived from ancestors[0]
 	var resource *AssetResource
 	if cai.Resource != nil {
 		resource = &AssetResource{
 			Version:              cai.Resource.Version,
 			DiscoveryDocumentURI: cai.Resource.DiscoveryDocumentURI,
 			DiscoveryName:        cai.Resource.DiscoveryName,
-			Parent:               fmt.Sprintf("//cloudresourcemanager.googleapis.com/projects/%v", project),
+			Parent:               parent,
 			Data:                 cai.Resource.Data,
 		}
 	}
@@ -425,7 +424,7 @@ func (c *Converter) augmentAsset(tfData resources.TerraformResourceData, cfg *re
 	return Asset{
 		Name:           cai.Name,
 		Type:           cai.Type,
-		Ancestry:       ancestry,
+		Ancestry:       ancestrymanager.ConvertToAncestryPath(ancestors),
 		Resource:       resource,
 		IAMPolicy:      policy,
 		OrgPolicy:      orgPolicy,
diff --git a/converters/google/vendor_utils.go b/converters/google/vendor_utils.go
deleted file mode 100644
index df161b32..00000000
--- a/converters/google/vendor_utils.go
+++ /dev/null
@@ -1,67 +0,0 @@
-// Copyright 2019 Google LLC
-//
-// 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.
-
-package google
-
-import (
-	"fmt"
-
-	resources "github.com/GoogleCloudPlatform/terraform-validator/converters/google/resources"
-	"go.uber.org/zap"
-)
-
-// NOTE: These functions were pulled from github.com/terraform-providers/terraform-provider-google. They can go away when the functionality they are providing is implemented in the future github.com/GoogleCloudPlatform/terraform-converters package.
-
-// getProject 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 getProject(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 != "" {
-		return res.(string), nil
-	}
-	if config.Project != "" {
-		return config.Project, nil
-	}
-	return "", fmt.Errorf("required field '%s' is not set, you may use --project=my-project to provide a default project to resolve the issue", projectSchemaField)
-}
diff --git a/go.mod b/go.mod
index 89823615..d92748b7 100644
--- a/go.mod
+++ b/go.mod
@@ -6,6 +6,7 @@ require (
 	github.com/apparentlymart/go-cidr v1.1.0
 	github.com/davecgh/go-spew v1.1.1
 	github.com/golang/protobuf v1.5.2
+	github.com/google/go-cmp v0.5.8
 	github.com/google/uuid v1.3.0 // indirect
 	github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
 	github.com/hashicorp/errwrap v1.0.0
diff --git a/converters/google/fake_resource_data.go b/tfdata/fake_resource_data.go
similarity index 97%
rename from converters/google/fake_resource_data.go
rename to tfdata/fake_resource_data.go
index e1b43752..a7b563a1 100644
--- a/converters/google/fake_resource_data.go
+++ b/tfdata/fake_resource_data.go
@@ -15,7 +15,7 @@
 // In order to interact with resource converters, we need to be able to create
 // "terraform resource data" that supports a very limited subset of the API actually
 // used during the conversion process.
-package google
+package tfdata
 
 import (
 	"fmt"
@@ -30,17 +30,15 @@ import (
 // Must be set to the same value as the internal typeObject const
 const typeObject schema.ValueType = 8
 
-
 // This is more or less equivalent to the internal getResult struct
 // used by schema.ResourceData
 type getResult struct {
-	Value interface{}
+	Value    interface{}
 	Computed bool
-	Exists bool
-	Schema *schema.Schema
+	Exists   bool
+	Schema   *schema.Schema
 }
 
-
 // Compare to https://github.com/hashicorp/terraform-plugin-sdk/blob/97b4465/helper/schema/resource_data.go#L15
 type FakeResourceData struct {
 	reader schema.FieldReader
@@ -81,10 +79,10 @@ func (d *FakeResourceData) get(addr []string) getResult {
 	}
 
 	return getResult{
-		Value: r.Value,
+		Value:    r.Value,
 		Computed: r.Computed,
-		Exists: r.Exists,
-		Schema: s,
+		Exists:   r.Exists,
+		Schema:   s,
 	}
 }
 
@@ -129,7 +127,7 @@ func (d *FakeResourceData) SetId(string)                      {}
 func (d *FakeResourceData) GetProviderMeta(interface{}) error { return nil }
 func (d *FakeResourceData) Timeout(key string) time.Duration  { return time.Duration(1) }
 
-func NewFakeResourceData(kind string, resourceSchema map[string]*schema.Schema, values map[string]interface{}) FakeResourceData {
+func NewFakeResourceData(kind string, resourceSchema map[string]*schema.Schema, values map[string]interface{}) *FakeResourceData {
 	state := map[string]string{}
 	var address []string
 	attributes(values, address, state, resourceSchema)
@@ -137,7 +135,7 @@ func NewFakeResourceData(kind string, resourceSchema map[string]*schema.Schema,
 		Map:    schema.BasicMapReader(state),
 		Schema: resourceSchema,
 	}
-	return FakeResourceData{
+	return &FakeResourceData{
 		kind:   kind,
 		schema: resourceSchema,
 		reader: reader,
diff --git a/tfgcv/planned_assets.go b/tfgcv/planned_assets.go
index d0386142..3be11dec 100644
--- a/tfgcv/planned_assets.go
+++ b/tfgcv/planned_assets.go
@@ -20,7 +20,7 @@ import (
 	"io/ioutil"
 	"path/filepath"
 
-	"google.golang.org/api/cloudresourcemanager/v1"
+	"google.golang.org/api/cloudresourcemanager/v3"
 
 	"github.com/GoogleCloudPlatform/terraform-validator/ancestrymanager"
 	"github.com/GoogleCloudPlatform/terraform-validator/converters/google"
@@ -71,7 +71,7 @@ func newConverter(ctx context.Context, path, project string, ancestry map[string
 
 	var resourceManager *cloudresourcemanager.Service
 	if !offline {
-		resourceManager = cfg.NewResourceManagerClient(cfg.UserAgent())
+		resourceManager = cfg.NewResourceManagerV3Client(cfg.UserAgent())
 	}
 	ancestryManager, err := ancestrymanager.New(resourceManager, ancestry, errorLogger)
 	if err != nil {


Test patch:
diff --git a/.ci/cloudbuild-tests-integration.yaml b/.ci/cloudbuild-tests-integration.yaml
index 32f0842a..1d7586dd 100644
--- a/.ci/cloudbuild-tests-integration.yaml
+++ b/.ci/cloudbuild-tests-integration.yaml
@@ -22,9 +22,9 @@ steps:
   args: ['-c', 'mv /terraform/$_TERRAFORM_VERSION /bin/terraform && /usr/bin/make build && /usr/bin/make test-integration']
   env:
     - TEST_PROJECT=$_TEST_PROJECT
-    - TEST_FOLDER=$_TEST_FOLDER
+    - TEST_FOLDER_ID=$_TEST_FOLDER
     - TEST_ANCESTRY=$_TEST_ANCESTRY
-    - TEST_ORG=$_TEST_ORG
+    - TEST_ORG_ID=$_TEST_ORG
     - TERRAFORM_VERSION=$_TERRAFORM_VERSION
 tags:
 - 'ci'
diff --git a/.ci/cloudbuild-tests-unit.yaml b/.ci/cloudbuild-tests-unit.yaml
index e0c1ff17..ca969f23 100644
--- a/.ci/cloudbuild-tests-unit.yaml
+++ b/.ci/cloudbuild-tests-unit.yaml
@@ -26,9 +26,9 @@ steps:
   args: ['test']
   env:
     - TEST_PROJECT=$_TEST_PROJECT
-    - TEST_FOLDER=$_TEST_FOLDER
+    - TEST_FOLDER_ID=$_TEST_FOLDER
     - TEST_ANCESTRY=$_TEST_ANCESTRY
-    - TEST_ORG=$_TEST_ORG
+    - TEST_ORG_ID=$_TEST_ORG
     - 'GOPATH=/gopath'
   waitFor: ['build']
 tags:
diff --git a/ancestrymanager/ancestrymanager_test.go b/ancestrymanager/ancestrymanager_test.go
index 0e3026f2..47d41edd 100644
--- a/ancestrymanager/ancestrymanager_test.go
+++ b/ancestrymanager/ancestrymanager_test.go
@@ -2,13 +2,19 @@ package ancestrymanager
 
 import (
 	"context"
+	"fmt"
 	"net/http"
 	"net/http/httptest"
-	"regexp"
+	"strings"
 	"testing"
 
+	resources "github.com/GoogleCloudPlatform/terraform-validator/converters/google/resources"
+	"github.com/GoogleCloudPlatform/terraform-validator/tfdata"
+
+	"github.com/google/go-cmp/cmp"
+	provider "github.com/hashicorp/terraform-provider-google/google"
 	"go.uber.org/zap"
-	cloudresourcemanager "google.golang.org/api/cloudresourcemanager/v1"
+	cloudresourcemanager "google.golang.org/api/cloudresourcemanager/v3"
 	"google.golang.org/api/option"
 )
 
@@ -22,130 +28,771 @@ func newTestResourceManagerClient(opts []option.ClientOption) *cloudresourcemana
 	return rm
 }
 
-func TestAncestryPath(t *testing.T) {
+func TestGetAncestors(t *testing.T) {
+	ownerProject := "foo"
+	ownerAncestryPath := "organization/qux/folder/bar/project/foo"
+	anotherProject := "foo2"
+
+	// Setup a simple test server to mock the response of resource manager.
+	responses := map[string]*cloudresourcemanager.Project{
+		"projects/12345":     {Name: "projects/12345", Parent: "folders/bar"},
+		"projects/foo":       {Name: "projects/foo", Parent: "folders/bar"},
+		"folders/bar":        {Name: "folders/bar", Parent: "organizations/qux"},
+		"organizations/qux":  {Name: "organizations/qux", Parent: ""},
+		"projects/foo2":      {Name: "projects/foo2", Parent: "folders/bar2"},
+		"folders/bar2":       {Name: "folders/bar2", Parent: "organizations/qux2"},
+		"organizations/qux2": {Name: "organizations/qux2", Parent: ""},
+	}
+	ts := newAncestryManagerMockServer(t, responses)
+	defer ts.Close()
+
+	// option.WithEndpoint(ts.URL), option.WithoutAuthentication()
+	rm := newTestResourceManagerClient([]option.ClientOption{option.WithEndpoint(ts.URL), option.WithoutAuthentication()})
+
+	entries := map[string]string{
+		ownerProject: ownerAncestryPath,
+	}
+
+	p := provider.Provider()
+
+	// offline return errors when the cache cannot cover the request.
+	// online return errors when neither cache and mock server cannot cover the request.
 	cases := []struct {
-		name           string
-		input          []*cloudresourcemanager.Ancestor
-		expectedOutput string
+		name             string
+		data             resources.TerraformResourceData
+		asset            *resources.Asset
+		want             []string
+		parent           string
+		wantOnlineError  bool
+		wantOfflineError bool
 	}{
 		{
-			name:           "Empty",
-			input:          []*cloudresourcemanager.Ancestor{},
-			expectedOutput: "",
+			name: "owner project - project id",
+			data: tfdata.NewFakeResourceData(
+				"google_project",
+				p.ResourcesMap["google_project"].Schema,
+				map[string]interface{}{
+					"project_id": ownerProject,
+				},
+			),
+			asset: &resources.Asset{
+				Type: "cloudresourcemanager.googleapis.com/Project",
+			},
+			want:   []string{"projects/foo", "folders/bar", "organizations/qux"},
+			parent: "//cloudresourcemanager.googleapis.com/folders/bar",
+		},
+		{
+			name: "owner project - project",
+			data: tfdata.NewFakeResourceData(
+				"google_project",
+				p.ResourcesMap["google_project"].Schema,
+				map[string]interface{}{
+					"project": ownerProject,
+				},
+			),
+			asset: &resources.Asset{
+				Type: "cloudresourcemanager.googleapis.com/Project",
+			},
+			want:   []string{"projects/foo", "folders/bar", "organizations/qux"},
+			parent: "//cloudresourcemanager.googleapis.com/folders/bar",
+		},
+		{
+			name: "owner project - project number",
+			data: tfdata.NewFakeResourceData(
+				"google_project",
+				p.ResourcesMap["google_project"].Schema,
+				map[string]interface{}{
+					"number": "12345",
+				},
+			),
+			asset: &resources.Asset{
+				Type: "cloudresourcemanager.googleapis.com/Project",
+			},
+			want:             []string{"projects/12345", "folders/bar", "organizations/qux"},
+			wantOfflineError: true,
+			parent:           "//cloudresourcemanager.googleapis.com/folders/bar",
+		},
+		{
+			name: "owner project - project from config",
+			data: tfdata.NewFakeResourceData(
+				"google_project",
+				p.ResourcesMap["google_project"].Schema,
+				map[string]interface{}{},
+			),
+			asset: &resources.Asset{
+				Type: "cloudresourcemanager.googleapis.com/Project",
+			},
+			want:   []string{"projects/foo", "folders/bar", "organizations/qux"},
+			parent: "//cloudresourcemanager.googleapis.com/folders/bar",
+		},
+		{
+			name: "another project",
+			data: tfdata.NewFakeResourceData(
+				"google_project",
+				p.ResourcesMap["google_project"].Schema,
+				map[string]interface{}{
+					"project_id": anotherProject,
+				},
+			),
+			asset: &resources.Asset{
+				Type: "cloudresourcemanager.googleapis.com/Project",
+			},
+			want:             []string{"projects/foo2", "folders/bar2", "organizations/qux2"},
+			wantOfflineError: true,
+			parent:           "//cloudresourcemanager.googleapis.com/folders/bar2",
+		},
+		{
+			name: "owner folder",
+			data: tfdata.NewFakeResourceData(
+				"google_folder_iam_policy",
+				p.ResourcesMap["google_folder_iam_policy"].Schema,
+				map[string]interface{}{
+					"folder": "bar",
+				},
+			),
+			asset: &resources.Asset{
+				Type: "cloudresourcemanager.googleapis.com/Folder",
+			},
+			want:   []string{"folders/bar", "organizations/qux"},
+			parent: "//cloudresourcemanager.googleapis.com/organizations/qux",
 		},
 		{
-			name: "ProjectOrganization",
-			input: []*cloudresourcemanager.Ancestor{
-				{
-					ResourceId: &cloudresourcemanager.ResourceId{
-						Id:   "my-prj",
-						Type: "project",
-					},
+			name: "owner folder with prefix",
+			data: tfdata.NewFakeResourceData(
+				"google_folder_iam_policy",
+				p.ResourcesMap["google_folder_iam_policy"].Schema,
+				map[string]interface{}{
+					"folder": "folders/bar",
 				},
-				{
-					ResourceId: &cloudresourcemanager.ResourceId{
-						Id:   "my-org",
-						Type: "organization",
-					},
+			),
+			asset: &resources.Asset{
+				Type: "cloudresourcemanager.googleapis.com/Folder",
+			},
+			want:   []string{"folders/bar", "organizations/qux"},
+			parent: "//cloudresourcemanager.googleapis.com/organizations/qux",
+		},
+		{
+			name: "another folder online",
+			data: tfdata.NewFakeResourceData(
+				"google_folder_iam_policy",
+				p.ResourcesMap["google_folder_iam_policy"].Schema,
+				map[string]interface{}{
+					"folder": "bar2",
+				},
+			),
+			asset: &resources.Asset{
+				Type: "cloudresourcemanager.googleapis.com/Folder",
+			},
+			want:             []string{"folders/bar2", "organizations/qux2"},
+			wantOfflineError: true,
+			parent:           "//cloudresourcemanager.googleapis.com/organizations/qux2",
+		},
+		{
+			// Not supporting folder create resource yet.
+			name: "not exist folder online",
+			data: tfdata.NewFakeResourceData(
+				"google_folder_iam_policy",
+				p.ResourcesMap["google_folder_iam_policy"].Schema,
+				map[string]interface{}{
+					"folder": "notexist",
 				},
+			),
+			asset: &resources.Asset{
+				Type: "cloudresourcemanager.googleapis.com/Folder",
 			},
-			expectedOutput: "organization/my-org/project/my-prj",
+			wantOfflineError: true,
+			wantOnlineError:  true,
+		},
+		{
+			name: "owner org",
+			data: tfdata.NewFakeResourceData(
+				"google_organization_iam_policy",
+				p.ResourcesMap["google_organization_iam_policy"].Schema,
+				map[string]interface{}{
+					"org_id": "qux",
+				},
+			),
+			asset: &resources.Asset{
+				Type: "cloudresourcemanager.googleapis.com/Organization",
+			},
+			want:   []string{"organizations/qux"},
+			parent: "",
+		},
+		{
+			// organization do not have ancestors except itself
+			// hence offline also pass.
+			name: "another org",
+			data: tfdata.NewFakeResourceData(
+				"google_organization_iam_policy",
+				p.ResourcesMap["google_organization_iam_policy"].Schema,
+				map[string]interface{}{
+					"org_id": "qux2",
+				},
+			),
+			asset: &resources.Asset{
+				Type: "cloudresourcemanager.googleapis.com/Organization",
+			},
+			want:   []string{"organizations/qux2"},
+			parent: "",
+		},
+		{
+			name: "other resource with owner project",
+			data: tfdata.NewFakeResourceData(
+				"google_compute_disk",
+				p.ResourcesMap["google_compute_disk"].Schema,
+				map[string]interface{}{
+					"project": ownerProject,
+				},
+			),
+			asset: &resources.Asset{
+				Type: "cloudresourcemanager.googleapis.com/Disk",
+			},
+			want:   []string{"projects/foo", "folders/bar", "organizations/qux"},
+			parent: "//cloudresourcemanager.googleapis.com/projects/foo",
+		},
+		{
+			name: "other resource online with another project",
+			data: tfdata.NewFakeResourceData(
+				"google_compute_disk",
+				p.ResourcesMap["google_compute_disk"].Schema,
+				map[string]interface{}{
+					"project": anotherProject,
+				},
+			),
+			asset: &resources.Asset{
+				Type: "cloudresourcemanager.googleapis.com/Disk",
+			},
+			want:             []string{"projects/foo2", "folders/bar2", "organizations/qux2"},
+			wantOfflineError: true,
+			parent:           "//cloudresourcemanager.googleapis.com/projects/foo2",
+		},
+		{
+			name: "custom role with org",
+			data: tfdata.NewFakeResourceData(
+				"google_organization_iam_custom_role",
+				p.ResourcesMap["google_organization_iam_custom_role"].Schema,
+				map[string]interface{}{
+					"org_id": "qux",
+				},
+			),
+			asset: &resources.Asset{
+				Type: "iam.googleapis.com/Role",
+			},
+			want:   []string{"organizations/qux"},
+			parent: "//cloudresourcemanager.googleapis.com/organizations/qux",
+		},
+		{
+			name: "custom role with project",
+			data: tfdata.NewFakeResourceData(
+				"google_project_iam_custom_role",
+				p.ResourcesMap["google_project_iam_custom_role"].Schema,
+				map[string]interface{}{
+					"project": "foo",
+				},
+			),
+			asset: &resources.Asset{
+				Type: "iam.googleapis.com/Role",
+			},
+			want:   []string{"projects/foo", "folders/bar", "organizations/qux"},
+			parent: "//cloudresourcemanager.googleapis.com/projects/foo",
+		},
+		{
+			name: "new project in folder",
+			data: tfdata.NewFakeResourceData(
+				"google_project",
+				p.ResourcesMap["google_project"].Schema,
+				map[string]interface{}{
+					"folder_id":  "bar",
+					"project_id": "new-project",
+				},
+			),
+			asset: &resources.Asset{
+				Type: "cloudresourcemanager.googleapis.com/Project",
+			},
+			want:   []string{"projects/new-project", "folders/bar", "organizations/qux"},
+			parent: "//cloudresourcemanager.googleapis.com/folders/bar",
+		},
+		{
+			name: "new project in organization",
+			data: tfdata.NewFakeResourceData(
+				"google_project",
+				p.ResourcesMap["google_project"].Schema,
+				map[string]interface{}{
+					"org_id":     "qux",
+					"project_id": "new-project",
+				},
+			),
+			asset: &resources.Asset{
+				Type: "cloudresourcemanager.googleapis.com/Project",
+			},
+			want:   []string{"projects/new-project", "organizations/qux"},
+			parent: "//cloudresourcemanager.googleapis.com/organizations/qux",
+		},
+		{
+			// for new projects, if it cannot find ancestors in online mode,
+			// it just returns 403 error.
+			// offline will fail because no cloud resource manager.
+			name: "new project without org_id or folder_id",
+			data: tfdata.NewFakeResourceData(
+				"google_project",
+				p.ResourcesMap["google_project"].Schema,
+				map[string]interface{}{
+					"project_id": "new-project",
+				},
+			),
+			asset: &resources.Asset{
+				Type: "cloudresourcemanager.googleapis.com/Project",
+			},
+			wantOnlineError:  true,
+			wantOfflineError: true,
 		},
 	}
-
 	for _, c := range cases {
 		t.Run(c.name, func(t *testing.T) {
-			output := ancestryPath(c.input)
-			if output != c.expectedOutput {
-				t.Errorf("expected output %q, got %q", c.expectedOutput, output)
+			cfg := &resources.Config{
+				Project: ownerProject,
+			}
+			amOnline, err := newManager(rm, entries, zap.NewExample())
+			if err != nil {
+				t.Fatalf("failed to create online ancestry manager: %s", err)
+			}
+			got, parent, err := amOnline.Ancestors(cfg, c.data, c.asset)
+			if c.wantOnlineError {
+				if err == nil {
+					t.Fatalf("onlineMgr.Ancestors(%v, %v, %v) = nil, want = err", cfg, c.data, c.asset)
+				}
+			} else {
+				if err != nil {
+					t.Fatalf("onlineMgr.Ancestors(%v, %v, %v) = %s, want = nil", cfg, c.data, c.asset, err)
+				}
+				if parent != c.parent {
+					t.Errorf("onlineMgr.Ancestors(%v, %v, %v) parent = %s, want = %s", cfg, c.data, c.asset, parent, c.parent)
+				}
+				if diff := cmp.Diff(c.want, got); diff != "" {
+					t.Errorf("onlineMgr.Ancestors(%v, %v, %v) returned unexpected diff (-want +got):\n%s", cfg, c.data, c.asset, diff)
+				}
+			}
+
+			amOffline, err := newManager(nil, entries, zap.NewExample())
+			if err != nil {
+				t.Fatalf("failed to create offline ancestry manager: %s", err)
+			}
+			got, parent, err = amOffline.Ancestors(cfg, c.data, c.asset)
+			if c.wantOfflineError {
+				if err == nil {
+					t.Fatalf("offlineMgr.Ancestors(%v, %v, %v) = nil, want = err", cfg, c.data, c.asset)
+				}
+			} else {
+				if err != nil {
+					t.Fatalf("offlineMgr.Ancestors(%v, %v, %v) = %s, want = nil", cfg, c.data, c.asset, err)
+				}
+				if parent != c.parent {
+					t.Errorf("offlineMgr.Ancestors(%v, %v, %v) parent = %s, want = %s", cfg, c.data, c.asset, parent, c.parent)
+				}
+				if diff := cmp.Diff(c.want, got); diff != "" {
+					t.Errorf("offlineMgr.Ancestors(%v, %v, %v) returned unexpected diff (-want +got):\n%s", cfg, c.data, c.asset, diff)
+				}
 			}
 		})
 	}
 }
 
-func TestGetAncestry(t *testing.T) {
-	ownerProject := "foo"
-	ownerAncestry := "organization/qux/folder/bar"
-	ownerAncestryPath := "organization/qux/folder/bar/project/foo"
-	anotherProject := "foo2"
-
-	// Setup a simple test server to mock the response of resource manager.
-	cache := map[string][]*cloudresourcemanager.Ancestor{
-		ownerProject: []*cloudresourcemanager.Ancestor{
-			{ResourceId: &cloudresourcemanager.ResourceId{Id: "foo", Type: "project"}},
-			{ResourceId: &cloudresourcemanager.ResourceId{Id: "bar", Type: "folder"}},
-			{ResourceId: &cloudresourcemanager.ResourceId{Id: "qux", Type: "organization"}},
+func TestGetAncestorsWithCache(t *testing.T) {
+	tests := []struct {
+		name      string
+		input     string
+		cache     map[string][]string
+		responses map[string]*cloudresourcemanager.Project
+		want      []string
+		wantCache map[string][]string
+	}{
+		{
+			name:  "empty cache",
+			input: "projects/abc",
+			cache: make(map[string][]string),
+			responses: map[string]*cloudresourcemanager.Project{
+				"projects/abc": {Name: "projects/123", ProjectId: "abc", Parent: "folders/456"},
+				"folders/456":  {Name: "folders/456", Parent: "folders/789"},
+				"folders/789":  {Name: "folders/789", Parent: "organizations/321"},
+			},
+			want: []string{"projects/123", "folders/456", "folders/789", "organizations/321"},
+			wantCache: map[string][]string{
+				"projects/abc":      {"projects/123", "folders/456", "folders/789", "organizations/321"},
+				"projects/123":      {"projects/123", "folders/456", "folders/789", "organizations/321"},
+				"folders/456":       {"folders/456", "folders/789", "organizations/321"},
+				"folders/789":       {"folders/789", "organizations/321"},
+				"organizations/321": {"organizations/321"},
+			},
 		},
-		anotherProject: []*cloudresourcemanager.Ancestor{
-			{ResourceId: &cloudresourcemanager.ResourceId{Id: "foo2", Type: "project"}},
-			{ResourceId: &cloudresourcemanager.ResourceId{Id: "bar2", Type: "folder"}},
-			{ResourceId: &cloudresourcemanager.ResourceId{Id: "qux2", Type: "organization"}},
+		{
+			name:  "partial cache",
+			input: "projects/abc",
+			cache: map[string][]string{
+				"folders/789":       {"folders/789", "organizations/321"},
+				"organizations/321": {"organizations/321"},
+			},
+			responses: map[string]*cloudresourcemanager.Project{
+				"projects/abc": {Name: "projects/123", ProjectId: "abc", Parent: "folders/456"},
+				"folders/456":  {Name: "folders/456", Parent: "folders/789"},
+			},
+			want: []string{"projects/123", "folders/456", "folders/789", "organizations/321"},
+			wantCache: map[string][]string{
+				"projects/abc":      {"projects/123", "folders/456", "folders/789", "organizations/321"},
+				"projects/123":      {"projects/123", "folders/456", "folders/789", "organizations/321"},
+				"folders/456":       {"folders/456", "folders/789", "organizations/321"},
+				"folders/789":       {"folders/789", "organizations/321"},
+				"organizations/321": {"organizations/321"},
+			},
+		},
+		{
+			name:  "all response from cache",
+			input: "projects/abc",
+			cache: map[string][]string{
+				"projects/abc": {"projects/123", "folders/456", "folders/789", "organizations/321"},
+			},
+			responses: map[string]*cloudresourcemanager.Project{},
+			want:      []string{"projects/123", "folders/456", "folders/789", "organizations/321"},
+			wantCache: map[string][]string{
+				"projects/abc":      {"projects/123", "folders/456", "folders/789", "organizations/321"},
+				"projects/123":      {"projects/123", "folders/456", "folders/789", "organizations/321"},
+				"folders/456":       {"folders/456", "folders/789", "organizations/321"},
+				"folders/789":       {"folders/789", "organizations/321"},
+				"organizations/321": {"organizations/321"},
+			},
+		},
+		{
+			name:      "organization",
+			input:     "organizations/321",
+			cache:     map[string][]string{},
+			responses: map[string]*cloudresourcemanager.Project{},
+			want:      []string{"organizations/321"},
+			wantCache: map[string][]string{
+				"organizations/321": {"organizations/321"},
+			},
 		},
 	}
-	ts := newAncestryManagerMockServer(t, cache)
-	defer ts.Close()
-
-	// option.WithEndpoint(ts.URL), option.WithoutAuthentication()
-	rm := newTestResourceManagerClient([]option.ClientOption{option.WithEndpoint(ts.URL), option.WithoutAuthentication()})
-
-	entries := map[string]string{
-		ownerProject: ownerAncestry,
-	}
-	amOnline, err := New(rm, entries, zap.NewExample())
-	if err != nil {
-		t.Fatalf("failed to create online ancestry manager: %s", err)
-	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			ts := newAncestryManagerMockServer(t, test.responses)
+			defer ts.Close()
 
-	amOffline, err := New(nil, entries, zap.NewExample())
-	if err != nil {
-		t.Fatalf("failed to create offline ancestry manager: %s", err)
+			m := &manager{
+				errorLogger:     zap.NewExample(),
+				resourceManager: newTestResourceManagerClient([]option.ClientOption{option.WithEndpoint(ts.URL), option.WithoutAuthentication()}),
+				ancestorCache:   test.cache,
+			}
+			got, err := m.getAncestorsWithCache(test.input)
+			if err != nil {
+				t.Fatalf("getAncestorsWithCache(%s) = %s, want = nil", test.input, err)
+			}
+			if diff := cmp.Diff(test.want, got); diff != "" {
+				t.Errorf("getAncestorsWithCache(%v) returned unexpected diff (-want +got):\n%s", test.input, diff)
+			}
+			if diff := cmp.Diff(test.wantCache, m.ancestorCache); diff != "" {
+				t.Errorf("getAncestorsWithCache(%v) cache returned unexpected diff (-want +got):\n%s", test.input, diff)
+			}
+		})
 	}
+}
 
-	cases := []struct {
+func TestGetAncestorsWithCache_Fail(t *testing.T) {
+	tests := []struct {
 		name      string
-		target    AncestryManager
-		query     string
-		wantError bool
-		want      string
+		input     string
+		cache     map[string][]string
+		responses map[string]*cloudresourcemanager.Project
+		wantErr   string
 	}{
-		{name: "owner_project_online", target: amOnline, query: ownerProject, want: ownerAncestryPath},
-		{name: "owner_project_offline", target: amOffline, query: ownerProject, want: ownerAncestryPath},
-		{name: "another_project_online", target: amOnline, query: anotherProject, want: "organization/qux2/folder/bar2/project/foo2"},
-		{name: "another_project_offline", target: amOffline, query: anotherProject, wantError: true},
-		{name: "missed_project_online", target: amOnline, query: "notexist", wantError: true},
+		{
+			name:  "no parent response",
+			input: "projects/abc",
+			cache: make(map[string][]string),
+			responses: map[string]*cloudresourcemanager.Project{
+				"projects/abc": {Name: "projects/123", ProjectId: "projects/abc", Parent: "folders/not-exist"},
+			},
+			wantErr: "no response",
+		},
 	}
-	for _, c := range cases {
-		t.Run(c.name, func(t *testing.T) {
-			got, err := c.target.GetAncestry(c.query)
-			if !c.wantError && err != nil {
-				t.Fatalf("GetAncestry(%s) returns error: %s", c.query, err)
-			}
-			if c.wantError && err == nil {
-				t.Fatalf("GetAncestry(%s) returns no error, want error", c.query)
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			ts := newAncestryManagerMockServer(t, test.responses)
+			defer ts.Close()
+
+			crm := newTestResourceManagerClient(
+				[]option.ClientOption{
+					option.WithEndpoint(ts.URL),
+					option.WithoutAuthentication(),
+				},
+			)
+			m := &manager{
+				errorLogger:     zap.NewExample(),
+				resourceManager: crm,
+				ancestorCache:   test.cache,
 			}
-			if got != c.want {
-				t.Errorf("GetAncestry(%s): got=%s, want=%s", c.query, got, c.want)
+			_, err := m.getAncestorsWithCache(test.input)
+			if err == nil {
+				t.Fatalf("getAncestorsWithCache(%s) = nil, want = %s", test.input, test.wantErr)
 			}
 		})
 	}
 }
 
-func newAncestryManagerMockServer(t *testing.T, cache map[string][]*cloudresourcemanager.Ancestor) *httptest.Server {
+func newAncestryManagerMockServer(t *testing.T, responses map[string]*cloudresourcemanager.Project) *httptest.Server {
 	t.Helper()
 	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		re := regexp.MustCompile(`([^/]*):getAncestry`)
-		path := re.FindStringSubmatch(r.URL.Path)
-		if path == nil || cache[path[1]] == nil {
+		name := strings.TrimPrefix(r.URL.Path, "/v3/")
+		resp, ok := responses[name]
+		if !ok {
 			w.WriteHeader(http.StatusForbidden)
+			w.Write([]byte(fmt.Sprintf("no response for request path %s", "/v3/"+name)))
 			return
 		}
-		payload, err := (&cloudresourcemanager.GetAncestryResponse{Ancestor: cache[path[1]]}).MarshalJSON()
+		payload, err := resp.MarshalJSON()
 		if err != nil {
-			t.Errorf("failed to MarshalJSON: %s", err)
 			w.WriteHeader(http.StatusInternalServerError)
+			w.Write([]byte(fmt.Sprintf("failed to MarshalJSON: %s", err)))
 			return
 		}
 		w.Write(payload)
 	}))
 	return ts
 }
+
+func TestParseAncestryPath(t *testing.T) {
+	tests := []struct {
+		name string
+		path string
+		want []string
+	}{
+		{
+			name: "all kinds of resource",
+			path: "organizations/123/folders/456/projects/789",
+			want: []string{"projects/789", "folders/456", "organizations/123"},
+		},
+		{
+			name: "multiple folders",
+			path: "organizations/123/folders/456/folders/789",
+			want: []string{"folders/789", "folders/456", "organizations/123"},
+		},
+		{
+			name: "normalize resource name",
+			path: "organization/123/folder/456/project/789",
+			want: []string{"projects/789", "folders/456", "organizations/123"},
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			got, err := parseAncestryPath(test.path)
+			if err != nil {
+				t.Fatalf("parseAncestryPath(%s) = %s, want = nil", test.path, err)
+			}
+			if diff := cmp.Diff(test.want, got); diff != "" {
+				t.Errorf("parseAncestryPath(%v) returned unexpected diff (-want +got):\n%s", test.path, diff)
+			}
+		})
+	}
+}
+
+func TestParseAncestryPath_Fail(t *testing.T) {
+	tests := []struct {
+		name    string
+		path    string
+		wantErr string
+	}{
+		{
+			name:    "malform with single word",
+			path:    "organizations",
+			wantErr: "unexpected format",
+		},
+		{
+			name:    "malform",
+			path:    "organizations/123/folders",
+			wantErr: "unexpected format",
+		},
+		{
+			name:    "invalid keyword",
+			path:    "org/123/folders/123",
+			wantErr: "invalid ancestry path",
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			_, err := parseAncestryPath(test.path)
+			if err == nil {
+				t.Fatalf("parseAncestryPath(%s) = nil, want = %s", test.path, test.wantErr)
+			}
+		})
+	}
+}
+
+func TestInitAncestryCache(t *testing.T) {
+	tests := []struct {
+		name    string
+		entries map[string]string
+		want    map[string][]string
+	}{
+		{
+			name: "empty ancestry",
+			entries: map[string]string{
+				"test-proj": "",
+			},
+			want: map[string][]string{},
+		},
+		{
+			name: "empty key",
+			entries: map[string]string{
+				"": "organizations/123/folders/345",
+			},
+			want: map[string][]string{},
+		},
+		{
+			name: "default key to project",
+			entries: map[string]string{
+				"test-proj": "organizations/123/folders/345",
+			},
+			want: map[string][]string{
+				"projects/test-proj": {"projects/test-proj", "folders/345", "organizations/123"},
+				"folders/345":        {"folders/345", "organizations/123"},
+				"organizations/123":  {"organizations/123"},
+			},
+		},
+		{
+			name: "key has prefix folders/",
+			entries: map[string]string{
+				"folders/345": "organizations/123",
+			},
+			want: map[string][]string{
+				"folders/345":       {"folders/345", "organizations/123"},
+				"organizations/123": {"organizations/123"},
+			},
+		},
+		{
+			name: "key has prefix projects/",
+			entries: map[string]string{
+				"projects/test-proj": "organizations/123",
+			},
+			want: map[string][]string{
+				"projects/test-proj": {"projects/test-proj", "organizations/123"},
+				"organizations/123":  {"organizations/123"},
+			},
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			m := &manager{
+				ancestorCache: make(map[string][]string),
+			}
+			err := m.initAncestryCache(test.entries)
+			if err != nil {
+				t.Fatalf("initAncestryCache(%v) = %s, want = nil", test.entries, err)
+			}
+			if diff := cmp.Diff(test.want, m.ancestorCache); diff != "" {
+				t.Errorf("initAncestryCache(%v) returned unexpected diff (-want +got):\n%s", test.entries, diff)
+			}
+		})
+	}
+}
+
+func TestInitAncestryCache_Fail(t *testing.T) {
+	tests := []struct {
+		name    string
+		entries map[string]string
+	}{
+		{
+			name: "typo",
+			entries: map[string]string{
+				"foldres/def": "organizations/123",
+			},
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			m := &manager{
+				ancestorCache: make(map[string][]string),
+			}
+			err := m.initAncestryCache(test.entries)
+			if err == nil {
+				t.Fatalf("initAncestryCache(%v) = nil, want = err", test.entries)
+			}
+		})
+	}
+}
+
+func TestParseAncestryKey(t *testing.T) {
+	tests := []struct {
+		name string
+		key  string
+		want string
+	}{
+		{
+			name: "not contain /",
+			key:  "proj",
+			want: "projects/proj",
+		},
+		{
+			name: "contain projects/",
+			key:  "projects/1",
+			want: "projects/1",
+		},
+		{
+			name: "contain folders/",
+			key:  "folders/1",
+			want: "folders/1",
+		},
+		{
+			name: "contain organizations/",
+			key:  "organizations/1",
+			want: "organizations/1",
+		},
+		{
+			name: "contain project/",
+			key:  "project/1",
+			want: "projects/1",
+		},
+		{
+			name: "contain folder/",
+			key:  "folder/1",
+			want: "folders/1",
+		},
+		{
+			name: "contain organization/",
+			key:  "organization/1",
+			want: "organizations/1",
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			got, err := parseAncestryKey(test.key)
+			if err != nil {
+				t.Fatalf("parseAncestryKey(%v) = %v, want = nil", test.key, err)
+			}
+			if got != test.want {
+				t.Errorf("parseAncestryKey(%v) = %v, want = %v", test.key, got, test.want)
+			}
+		})
+	}
+}
+
+func TestParseAncestryKey_Fail(t *testing.T) {
+	tests := []struct {
+		name string
+		key  string
+	}{
+		{
+			name: "invalid spell",
+			key:  "org/1",
+		},
+		{
+			name: "multiple /",
+			key:  "folders/123/folders/456",
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			got, err := parseAncestryKey(test.key)
+			if err == nil {
+				t.Fatalf("parseAncestryKey(%v) = %v, want error", test.key, got)
+			}
+		})
+	}
+}
diff --git a/ancestrymanager/ancestryutil_test.go b/ancestrymanager/ancestryutil_test.go
new file mode 100644
index 00000000..afd0d4dc
--- /dev/null
+++ b/ancestrymanager/ancestryutil_test.go
@@ -0,0 +1,147 @@
+package ancestrymanager
+
+import (
+	"testing"
+
+	resources "github.com/GoogleCloudPlatform/terraform-validator/converters/google/resources"
+)
+
+func TestAssetParent(t *testing.T) {
+	tests := []struct {
+		name      string
+		ancestors []string
+		tfData    resources.TerraformResourceData
+		cai       *resources.Asset
+		want      string
+		wantErr   bool
+	}{
+		{
+			name:      "project",
+			ancestors: []string{"projects/123", "folders/456", "organizations/789"},
+			cai:       &resources.Asset{Type: "cloudresourcemanager.googleapis.com/Project"},
+			want:      "//cloudresourcemanager.googleapis.com/folders/456",
+		},
+		{
+			// new project without org_id and folder_id cannot derive ancestor list
+			name:      "project with no other ancestors",
+			ancestors: []string{"projects/123"},
+			cai:       &resources.Asset{Type: "cloudresourcemanager.googleapis.com/Project"},
+			want:      "//cloudresourcemanager.googleapis.com/projects/123",
+		},
+		{
+			name:      "folder",
+			ancestors: []string{"folders/456", "organizations/789"},
+			cai:       &resources.Asset{Type: "cloudresourcemanager.googleapis.com/Folder"},
+			want:      "//cloudresourcemanager.googleapis.com/organizations/789",
+		},
+		{
+			name:      "organization",
+			ancestors: []string{"organizations/789"},
+			cai:       &resources.Asset{Type: "cloudresourcemanager.googleapis.com/Organization"},
+			want:      "",
+		},
+		{
+			name:      "other resource",
+			ancestors: []string{"projects/123", "folders/456", "organizations/789"},
+			cai:       &resources.Asset{Type: "storage.googleapis.com/Bucket"},
+			want:      "//cloudresourcemanager.googleapis.com/projects/123",
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			got, err := assetParent(test.cai, test.ancestors)
+			if err != nil {
+				t.Fatalf("AssetParent(%v, %v) = %v, want = nil", test.cai, test.ancestors, err)
+			}
+			if got != test.want {
+				t.Errorf("AssetParent(%v, %v) = %v, want = %v", test.cai, test.ancestors, got, test.want)
+			}
+		})
+	}
+}
+
+func TestAssetParent_Fail(t *testing.T) {
+	tests := []struct {
+		name      string
+		ancestors []string
+		tfData    resources.TerraformResourceData
+		cai       *resources.Asset
+	}{
+		{
+			name:      "project",
+			ancestors: []string{},
+			cai:       &resources.Asset{Type: "cloudresourcemanager.googleapis.com/Project"},
+		},
+		{
+			name:      "folder",
+			ancestors: []string{"folders/456"},
+			cai:       &resources.Asset{Type: "cloudresourcemanager.googleapis.com/Folder"},
+		},
+		{
+			name:      "other resource",
+			ancestors: []string{},
+			cai:       &resources.Asset{Type: "storage.googleapis.com/Bucket"},
+		},
+		{
+			name:      "invalid ancestor",
+			ancestors: []string{"abc/3"},
+		},
+		{
+			name:      "empty project ID",
+			ancestors: []string{"projects/"},
+		},
+		{
+			name:      "empty folder ID",
+			ancestors: []string{"folders/"},
+		},
+		{
+			name:      "empty organization ID",
+			ancestors: []string{"organizations/"},
+		},
+		{
+			name:      "asset not provided",
+			ancestors: []string{"organizations/123"},
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			_, err := assetParent(test.cai, test.ancestors)
+			if err == nil {
+				t.Fatalf("AssetParent(%v, %v) = nil, want = error", test.cai, test.ancestors)
+			}
+		})
+	}
+}
+
+func TestConvertToAncestryPath(t *testing.T) {
+	cases := []struct {
+		name           string
+		input          []string
+		expectedOutput string
+	}{
+		{
+			name:           "Empty",
+			input:          []string{},
+			expectedOutput: "",
+		},
+		{
+			name:           "ProjectOrganization",
+			input:          []string{"project/my-prj", "organization/my-org"},
+			expectedOutput: "organization/my-org/project/my-prj",
+		},
+		{
+			name:           "convert to existing ancestry path style",
+			input:          []string{"projects/my-prj", "folders/123", "organizations/my-org"},
+			expectedOutput: "organization/my-org/folder/123/project/my-prj",
+		},
+	}
+
+	for _, c := range cases {
+		t.Run(c.name, func(t *testing.T) {
+			output := ConvertToAncestryPath(c.input)
+			if output != c.expectedOutput {
+				t.Errorf("expected output %q, got %q", c.expectedOutput, output)
+			}
+		})
+	}
+}
diff --git a/test/init_test.go b/test/init_test.go
index 0334393f..39c279de 100644
--- a/test/init_test.go
+++ b/test/init_test.go
@@ -136,6 +136,10 @@ func normalizeAssets(t *testing.T, assets []google.Asset, offline bool) []google
 			// remove the ancestry as the value of that is dependent on project,
 			// and is not important for the test.
 			asset.Ancestry = ""
+			// remove the parent as the value of that is dependent on project.
+			if asset.Resource != nil {
+				asset.Resource.Parent = ""
+			}
 		}
 		// Replace placeholder in names. This allows us to compare generated placeholders
 		// (for example due to "unknown after apply") with the values in the expected
diff --git a/testdata/templates/example_folder_organization_policy.json b/testdata/templates/example_folder_organization_policy.json
index 20d7d9b8..456b70c0 100644
--- a/testdata/templates/example_folder_organization_policy.json
+++ b/testdata/templates/example_folder_organization_policy.json
@@ -2,7 +2,7 @@
   {
     "name": "//cloudresourcemanager.googleapis.com/folders/{{.FolderID}}",
     "asset_type": "cloudresourcemanager.googleapis.com/Folder",
-    "ancestry_path": "{{.Ancestry}}/project/{{.Provider.project}}",
+    "ancestry_path": "{{.Ancestry}}",
     "org_policy": [
       {
         "constraint": "constraints/compute.disableSerialPortAccess",
diff --git a/testdata/templates/example_organization_iam_binding.json b/testdata/templates/example_organization_iam_binding.json
index bb87161a..39f3329b 100644
--- a/testdata/templates/example_organization_iam_binding.json
+++ b/testdata/templates/example_organization_iam_binding.json
@@ -2,7 +2,7 @@
   {
     "name": "//cloudresourcemanager.googleapis.com/organizations/123456789",
     "asset_type": "cloudresourcemanager.googleapis.com/Organization",
-    "ancestry_path": "{{.Ancestry}}/project/{{.Provider.project}}",
+    "ancestry_path": "organization/123456789",
     "iam_policy": {
       "bindings": [
         {
diff --git a/testdata/templates/example_organization_iam_custom_role.json b/testdata/templates/example_organization_iam_custom_role.json
index 2af903ef..313ac867 100644
--- a/testdata/templates/example_organization_iam_custom_role.json
+++ b/testdata/templates/example_organization_iam_custom_role.json
@@ -2,12 +2,12 @@
   {
     "name": "//iam.googleapis.com/organizations/{{.OrgID}}/roles/myCustomRole",
     "asset_type": "iam.googleapis.com/Role",
-    "ancestry_path": "{{.Ancestry}}/project/{{.Provider.project}}",
+    "ancestry_path": "organization/{{.OrgID}}",
     "resource": {
       "version": "v1",
       "discovery_document_uri": "https://iam.googleapis.com/$discovery/rest?version=v1",
       "discovery_name": "Role",
-      "parent": "//cloudresourcemanager.googleapis.com/projects/{{.Provider.project}}",
+      "parent": "//cloudresourcemanager.googleapis.com/organizations/{{.OrgID}}",
       "data": {
         "description": "A description",
         "includedPermissions": [
diff --git a/testdata/templates/example_organization_iam_member.json b/testdata/templates/example_organization_iam_member.json
index 512cac57..da6bfa15 100644
--- a/testdata/templates/example_organization_iam_member.json
+++ b/testdata/templates/example_organization_iam_member.json
@@ -2,7 +2,7 @@
   {
     "name": "//cloudresourcemanager.googleapis.com/organizations/0123456789",
     "asset_type": "cloudresourcemanager.googleapis.com/Organization",
-    "ancestry_path": "{{.Ancestry}}/project/{{.Provider.project}}",
+    "ancestry_path": "organization/0123456789",
     "iam_policy": {
       "bindings": [
         {
diff --git a/testdata/templates/example_organization_iam_policy.json b/testdata/templates/example_organization_iam_policy.json
index 06cbbc7a..88aab321 100644
--- a/testdata/templates/example_organization_iam_policy.json
+++ b/testdata/templates/example_organization_iam_policy.json
@@ -2,7 +2,7 @@
   {
     "name": "//cloudresourcemanager.googleapis.com/organizations/123456789",
     "asset_type": "cloudresourcemanager.googleapis.com/Organization",
-    "ancestry_path": "{{.Ancestry}}/project/{{.Provider.project}}",
+    "ancestry_path": "organization/123456789",
     "iam_policy": {
       "bindings": [
         {
diff --git a/testdata/templates/example_organization_policy.json b/testdata/templates/example_organization_policy.json
index bed9f28a..45b58d6d 100644
--- a/testdata/templates/example_organization_policy.json
+++ b/testdata/templates/example_organization_policy.json
@@ -2,7 +2,7 @@
   {
     "name": "//cloudresourcemanager.googleapis.com/organizations/{{.OrgID}}",
     "asset_type": "cloudresourcemanager.googleapis.com/Organization",
-    "ancestry_path": "{{.Ancestry}}/project/{{.Provider.project}}",
+    "ancestry_path": "organization/{{.OrgID}}",
     "org_policy": [
       {
         "constraint": "constraints/compute.disableSerialPortAccess",
diff --git a/testdata/templates/example_project_create.json b/testdata/templates/example_project_create.json
index 95366f38..46784776 100644
--- a/testdata/templates/example_project_create.json
+++ b/testdata/templates/example_project_create.json
@@ -23,7 +23,7 @@
       "version": "v1",
       "discovery_document_uri": "https://www.googleapis.com/discovery/v1/apis/compute/v1/rest",
       "discovery_name": "Project",
-      "parent": "//cloudresourcemanager.googleapis.com/projects/{{.Provider.project}}",
+      "parent": "//cloudresourcemanager.googleapis.com/folders/{{.FolderID}}",
       "data": {
         "name": "My Project",
         "labels": {
diff --git a/testdata/templates/example_project_in_folder.json b/testdata/templates/example_project_in_folder.json
index 1dc2440e..f652c09f 100644
--- a/testdata/templates/example_project_in_folder.json
+++ b/testdata/templates/example_project_in_folder.json
@@ -1,22 +1,21 @@
 [
-    {
-      "name": "//cloudresourcemanager.googleapis.com/projects/foobat",
-      "asset_type": "cloudresourcemanager.googleapis.com/Project",
-      "ancestry_path": "organization/unknown/folder/{{.FolderID}}/project/foobat",
-      "resource": {
-        "version": "v1",
-        "discovery_document_uri": "https://www.googleapis.com/discovery/v1/apis/compute/v1/rest",
-        "discovery_name": "Project",
-        "parent": "//cloudresourcemanager.googleapis.com/projects/foobat",
-        "data": {
-          "name": "My Project",
-          "projectId": "foobat",
-          "parent": {
-            "type": "folder",
-            "id": "{{.FolderID}}"
-          }
+  {
+    "name": "//cloudresourcemanager.googleapis.com/projects/foobat",
+    "asset_type": "cloudresourcemanager.googleapis.com/Project",
+    "ancestry_path": "{{.Ancestry}}/project/foobat",
+    "resource": {
+      "version": "v1",
+      "discovery_document_uri": "https://www.googleapis.com/discovery/v1/apis/compute/v1/rest",
+      "discovery_name": "Project",
+      "parent": "//cloudresourcemanager.googleapis.com/folders/{{.FolderID}}",
+      "data": {
+        "name": "My Project",
+        "projectId": "foobat",
+        "parent": {
+          "type": "folder",
+          "id": "{{.FolderID}}"
         }
       }
     }
-  ]
-  
\ No newline at end of file
+  }
+]
diff --git a/testdata/templates/example_project_in_org.json b/testdata/templates/example_project_in_org.json
index 39a72793..0e8ad5f9 100644
--- a/testdata/templates/example_project_in_org.json
+++ b/testdata/templates/example_project_in_org.json
@@ -1,22 +1,21 @@
 [
-    {
-      "name": "//cloudresourcemanager.googleapis.com/projects/foobat",
-      "asset_type": "cloudresourcemanager.googleapis.com/Project",
-      "ancestry_path": "organization/{{.OrgID}}/project/foobat",
-      "resource": {
-        "version": "v1",
-        "discovery_document_uri": "https://www.googleapis.com/discovery/v1/apis/compute/v1/rest",
-        "discovery_name": "Project",
-        "parent": "//cloudresourcemanager.googleapis.com/projects/foobat",
-        "data": {
-          "name": "My Project",
-          "projectId": "foobat",
-          "parent": {
-            "type": "organization",
-            "id": "{{.OrgID}}"
-          }
+  {
+    "name": "//cloudresourcemanager.googleapis.com/projects/foobat",
+    "asset_type": "cloudresourcemanager.googleapis.com/Project",
+    "ancestry_path": "organization/{{.OrgID}}/project/foobat",
+    "resource": {
+      "version": "v1",
+      "discovery_document_uri": "https://www.googleapis.com/discovery/v1/apis/compute/v1/rest",
+      "discovery_name": "Project",
+      "parent": "//cloudresourcemanager.googleapis.com/organizations/{{.OrgID}}",
+      "data": {
+        "name": "My Project",
+        "projectId": "foobat",
+        "parent": {
+          "type": "organization",
+          "id": "{{.OrgID}}"
         }
       }
     }
-  ]
-  
\ No newline at end of file
+  }
+]
diff --git a/testdata/templates/example_project_organization_policy.json b/testdata/templates/example_project_organization_policy.json
index 665f0b5f..3a988bd0 100644
--- a/testdata/templates/example_project_organization_policy.json
+++ b/testdata/templates/example_project_organization_policy.json
@@ -9,19 +9,19 @@
           "enforced": true
         },
         "update_time": "{{.Time.RFC3339Nano}}"
-    },
+      },
       {
         "constraint": "constraints/iam.disableServiceAccountCreation",
         "restore_default": {},
         "update_time": "{{.Time.RFC3339Nano}}"
-    },
+      },
       {
         "constraint": "constraints/serviceuser.services",
         "list_policy": {
           "all_values": 1
         },
         "update_time": "{{.Time.RFC3339Nano}}"
-    }
+      }
     ],
     "ancestry_path": "{{.Ancestry}}/project/{{.Provider.project}}"
   }
diff --git a/testdata/templates/example_project_update.json b/testdata/templates/example_project_update.json
index 8981ec54..72d9102c 100644
--- a/testdata/templates/example_project_update.json
+++ b/testdata/templates/example_project_update.json
@@ -2,7 +2,7 @@
   {
     "name": "//cloudbilling.googleapis.com/projects/{{.Project.Number}}/billingInfo",
     "asset_type": "cloudbilling.googleapis.com/ProjectBillingInfo",
-    "ancestry_path": "organization/{{.OrgID}}/project/{{.Project.Number}}",
+    "ancestry_path": "organization/12345/project/{{.Project.Number}}",
     "resource": {
       "version": "v1",
       "discovery_document_uri": "https://www.googleapis.com/discovery/v1/apis/cloudbilling/v1/rest",
@@ -18,23 +18,23 @@
   {
     "name": "//cloudresourcemanager.googleapis.com/projects/{{.Project.Number}}",
     "asset_type": "cloudresourcemanager.googleapis.com/Project",
-    "ancestry_path": "organization/{{.OrgID}}/project/{{.Project.Number}}",
+    "ancestry_path": "organization/12345/project/{{.Project.Number}}",
     "resource": {
       "version": "v1",
       "discovery_document_uri": "https://www.googleapis.com/discovery/v1/apis/compute/v1/rest",
       "discovery_name": "Project",
-      "parent": "//cloudresourcemanager.googleapis.com/projects/{{.Project.Number}}",
+      "parent": "//cloudresourcemanager.googleapis.com/organizations/12345",
       "data": {
         "labels": {
           "project-label-key-a": "project-label-val-a"
         },
         "name": "My New Project",
         "parent": {
-          "id": "{{.OrgID}}",
+          "id": "12345",
           "type": "organization"
         },
         "projectId": "{{.Provider.project}}"
       }
     }
   }
-]
\ No newline at end of file
+]
diff --git a/testdata/templates/example_project_update.tf b/testdata/templates/example_project_update.tf
index 0bd91a4a..497c48e6 100644
--- a/testdata/templates/example_project_update.tf
+++ b/testdata/templates/example_project_update.tf
@@ -30,7 +30,7 @@ provider "google" {
 resource "google_project" "my_project" {
   name = "My New Project"
   project_id = "{{.Provider.project}}"
-  org_id  = "{{.OrgID}}"
+  org_id  = "12345"
 
   billing_account = "{{.Project.BillingAccountName}}"
 
diff --git a/testdata/templates/example_project_update.tfplan.json b/testdata/templates/example_project_update.tfplan.json
index 00029671..5cb1da14 100644
--- a/testdata/templates/example_project_update.tfplan.json
+++ b/testdata/templates/example_project_update.tfplan.json
@@ -21,7 +21,7 @@
             },
             "name": "My New Project",
             "number": "{{.Project.Number}}",
-            "org_id": "{{.OrgID}}",
+            "org_id": "12345",
             "project_id": "{{.Provider.project}}",
             "skip_delete": null,
             "timeouts": null
@@ -51,7 +51,7 @@
           },
           "name": "My Project",
           "number": "{{.Project.Number}}",
-          "org_id": "{{.OrgID}}",
+          "org_id": "12345",
           "project_id": "{{.Provider.project}}",
           "skip_delete": null,
           "timeouts": null
@@ -66,7 +66,7 @@
           },
           "name": "My New Project",
           "number": "{{.Project.Number}}",
-          "org_id": "{{.OrgID}}",
+          "org_id": "12345",
           "project_id": "{{.Provider.project}}",
           "skip_delete": null,
           "timeouts": null
@@ -98,7 +98,7 @@
               },
               "name": "My Project",
               "number": "{{.Project.Number}}",
-              "org_id": "{{.OrgID}}",
+              "org_id": "12345",
               "project_id": "{{.Provider.project}}",
               "skip_delete": null,
               "timeouts": null
@@ -122,7 +122,7 @@
               "constant_value": "{{.Project.BillingAccountName}}"
             },
             "org_id": {
-              "constant_value": "organizations/{{.OrgID}}"
+              "constant_value": "organizations/12345"
             },
             "labels": {
               "constant_value": {
diff --git a/testdata/templates/example_project_update.tfstate b/testdata/templates/example_project_update.tfstate
index 72f29ace..df8f510c 100644
--- a/testdata/templates/example_project_update.tfstate
+++ b/testdata/templates/example_project_update.tfstate
@@ -23,7 +23,7 @@
             },
             "name": "My Project",
             "number": "{{.Project.Number}}",
-            "org_id": "{{.OrgID}}",
+            "org_id": "12345",
             "project_id": "{{.Provider.project}}",
             "skip_delete": null,
             "timeouts": null
diff --git a/converters/google/fake_resource_data_test.go b/tfdata/fake_resource_data_test.go
similarity index 99%
rename from converters/google/fake_resource_data_test.go
rename to tfdata/fake_resource_data_test.go
index 5e69e81d..994f853d 100644
--- a/converters/google/fake_resource_data_test.go
+++ b/tfdata/fake_resource_data_test.go
@@ -11,7 +11,7 @@
 // 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.
-package google
+package tfdata
 
 import (
 	"testing"
diff --git a/tfgcv/planned_assets_test.go b/tfgcv/planned_assets_test.go
index 38caa7e7..5c8ba4c1 100644
--- a/tfgcv/planned_assets_test.go
+++ b/tfgcv/planned_assets_test.go
@@ -11,7 +11,7 @@ import (
 const (
 	testDataDir      = "../test/read_planned_assets"
 	testProjectName  = "gl-akopachevskyy-sql-db"
-	testAncestryName = "ancestry"
+	testAncestryName = "folders/1234"
 )
 
 type args struct {
@@ -21,10 +21,11 @@ type args struct {
 	convertUnchanged bool
 }
 type testcase struct {
-	name    string
-	args    args
-	want    int
-	wantErr bool
+	name          string
+	args          args
+	want          int
+	wantErr       bool
+	ancestryCache map[string]string
 }
 
 func testCases(t *testing.T) []testcase {
@@ -36,36 +37,62 @@ func testCases(t *testing.T) []testcase {
 			args{"tf0_12plan.json", testProjectName, testAncestryName, false},
 			2,
 			false,
+			map[string]string{
+				"projects/" + testProjectName: testAncestryName + "/projects/" + testProjectName,
+			},
 		},
 		{
 			"Test TF0_12 with all coverage",
 			args{"tf0_12plan.allcoverage.json", "foobar", testAncestryName, false},
 			9,
 			false,
+			map[string]string{
+				"projects/foobar":         testAncestryName,
+				"folders/my-folder":       "organization/test-org",
+				"organizations/123456789": "",
+			},
 		},
 		{
 			"Test TF0_12 with no-op",
 			args{"tf0_12plan.applied.json", "foobar", testAncestryName, true},
 			6,
 			false,
+			map[string]string{
+				"projects/345":    testAncestryName,
+				"projects/foobar": testAncestryName,
+				"folders/567":     "organization/123",
+			},
 		},
 		{
 			"Test TF1_0 and JSON plan",
 			args{"tf1_0plan.json", testProjectName, testAncestryName, false},
 			2,
 			false,
+			map[string]string{
+				"projects/" + testProjectName: testAncestryName,
+				"folders/my-folder":           "organization/test-org",
+			},
 		},
 		{
 			"Test TF1_0 with all coverage",
 			args{"tf1_0plan.allcoverage.json", "foobar", testAncestryName, false},
 			9,
 			false,
+			map[string]string{
+				"projects/foobar":   testAncestryName,
+				"folders/my-folder": "organization/test-org",
+			},
 		},
 		{
 			"Test TF1_0 with no-op",
 			args{"tf1_0plan.applied.json", "foobar", testAncestryName, true},
 			6,
 			false,
+			map[string]string{
+				"projects/345":    testAncestryName,
+				"projects/foobar": testAncestryName,
+				"folders/567":     "organization/123",
+			},
 		},
 	}...)
 	return tests
@@ -77,10 +104,7 @@ func TestReadPlannedAssets(t *testing.T) {
 			testFile := filepath.Join(testDataDir, tt.args.file)
 			offline := true
 			ctx := context.Background()
-			ancestryCache := map[string]string{
-				tt.args.project: tt.args.ancestry,
-			}
-			got, err := ReadPlannedAssets(ctx, testFile, tt.args.project, ancestryCache, offline, tt.args.convertUnchanged, zap.NewExample(), "")
+			got, err := ReadPlannedAssets(ctx, testFile, tt.args.project, tt.ancestryCache, offline, tt.args.convertUnchanged, zap.NewExample(), "")
 			if (err != nil) != tt.wantErr {
 				t.Errorf("ReadPlannedAssets() error = %v, wantErr %v", err, tt.wantErr)
 				return