Repository: GoogleCloudPlatform/terraform-validator
Pull request #468 (2022-01-18 14:30:58)

Add output option for convert 
# Background
User perceived tfv stall because it takes long at validation. There proposed a solution to separate convert and validate for gcloud to execute them sequentially. For convert, it will stream the output, and for validate, it uses a progress tracker to show loading animation.
 

# PR
The plan is for convert command to write the result into a file, and validate command to take that file as the input. As a result, the PR does the following. 
- For convert subcommand, add output option. If that output option is not empty, write the converted content into a file. 
- For validate subcommand, support reading converted asset file 


Patch:
diff --git a/cmd/convert.go b/cmd/convert.go
index d587a9ae..1c2e5289 100644
--- a/cmd/convert.go
+++ b/cmd/convert.go
@@ -46,6 +46,7 @@ type convertOptions struct {
 	offline           bool
 	rootOptions       *rootOptions
 	readPlannedAssets tfgcv.ReadPlannedAssetsFunc
+	outputPath        string
 }
 
 func newConvertCmd(rootOptions *rootOptions) *cobra.Command {
@@ -69,6 +70,7 @@ func newConvertCmd(rootOptions *rootOptions) *cobra.Command {
 	cmd.Flags().StringVar(&o.project, "project", "", "Provider project override (override the default project configuration assigned to the google terraform provider when converting resources)")
 	cmd.Flags().StringVar(&o.ancestry, "ancestry", "", "Override the ancestry location of the project when validating resources")
 	cmd.Flags().BoolVar(&o.offline, "offline", false, "Do not make network requests")
+	cmd.Flags().StringVar(&o.outputPath, "output-path", "", "If specified, write the convert result into the specified output file")
 
 	return cmd
 }
@@ -93,6 +95,19 @@ func (o *convertOptions) run(plan string) error {
 		return errors.Wrap(err, "converting tfplan to CAI assets")
 	}
 
+	if len(o.outputPath) > 0 {
+		f, err := os.OpenFile(o.outputPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0666)
+		if err != nil {
+			return err
+		}
+		defer f.Close()
+
+		if err := json.NewEncoder(f).Encode(assets); err != nil {
+			return errors.Wrap(err, "encoding json")
+		}
+		return nil
+	}
+
 	if o.rootOptions.useStructuredLogging {
 		o.rootOptions.outputLogger.Info(
 			"converted resources",
@@ -105,6 +120,5 @@ func (o *convertOptions) run(plan string) error {
 	if err := json.NewEncoder(os.Stdout).Encode(assets); err != nil {
 		return errors.Wrap(err, "encoding json")
 	}
-
 	return nil
 }
diff --git a/cmd/validate.go b/cmd/validate.go
index 66ff2ca4..d1bd4ccd 100644
--- a/cmd/validate.go
+++ b/cmd/validate.go
@@ -16,11 +16,14 @@ package cmd
 
 import (
 	"context"
+	"encoding/json"
 	"fmt"
+	"io/ioutil"
 	"os"
 
-	"github.com/GoogleCloudPlatform/terraform-validator/tfgcv"
 	"github.com/GoogleCloudPlatform/config-validator/pkg/api/validator"
+	"github.com/GoogleCloudPlatform/terraform-validator/converters/google"
+	"github.com/GoogleCloudPlatform/terraform-validator/tfgcv"
 	"github.com/golang/protobuf/jsonpb"
 	"github.com/pkg/errors"
 	"github.com/spf13/cobra"
@@ -29,7 +32,7 @@ import (
 
 const validateDesc = `
 Validate that a terraform plan conforms to a Constraint Framework
-policy library written to expect Google CAI (Cloud Asset Inventory) data. 
+policy library written to expect Google CAI (Cloud Asset Inventory) data.
 Unsupported terraform resources (see: "terraform-validate list-supported-resources")
 are skipped.
 
@@ -100,12 +103,22 @@ func (o *validateOptions) validateArgs(args []string) error {
 
 func (o *validateOptions) run(plan string) error {
 	ctx := context.Background()
-	assets, err := o.readPlannedAssets(ctx, plan, o.project, o.ancestry, o.offline, false, o.rootOptions.errorLogger)
+
+	content, err := ioutil.ReadFile(plan)
 	if err != nil {
-		if errors.Cause(err) == tfgcv.ErrParsingProviderProject {
-			return errors.New("unable to parse provider project, please use --project flag")
+		return fmt.Errorf("unable to read file %s", plan)
+	}
+	// if input file is not Asset, try convert
+	var assets []google.Asset
+	if err := json.Unmarshal(content, &assets); err != nil {
+		var err error
+		assets, err = o.readPlannedAssets(ctx, plan, o.project, o.ancestry, o.offline, false, o.rootOptions.errorLogger)
+		if err != nil {
+			if errors.Cause(err) == tfgcv.ErrParsingProviderProject {
+				return errors.New("unable to parse provider project, please use --project flag")
+			}
+			return errors.Wrap(err, "converting tfplan to CAI assets")
 		}
-		return errors.Wrap(err, "converting tfplan to CAI assets")
 	}
 
 	violations, err := o.validateAssets(ctx, assets, o.policyPath)
diff --git a/converters/google/resources/apigee_instance.go b/converters/google/resources/apigee_instance.go
index 92a329fa..ac51cda1 100644
--- a/converters/google/resources/apigee_instance.go
+++ b/converters/google/resources/apigee_instance.go
@@ -66,6 +66,12 @@ func GetApigeeInstanceApiObject(d TerraformResourceData, config *Config) (map[st
 	} else if v, ok := d.GetOkExists("peering_cidr_range"); !isEmptyValue(reflect.ValueOf(peeringCidrRangeProp)) && (ok || !reflect.DeepEqual(v, peeringCidrRangeProp)) {
 		obj["peeringCidrRange"] = peeringCidrRangeProp
 	}
+	ipRangeProp, err := expandApigeeInstanceIpRange(d.Get("ip_range"), d, config)
+	if err != nil {
+		return nil, err
+	} else if v, ok := d.GetOkExists("ip_range"); !isEmptyValue(reflect.ValueOf(ipRangeProp)) && (ok || !reflect.DeepEqual(v, ipRangeProp)) {
+		obj["ipRange"] = ipRangeProp
+	}
 	descriptionProp, err := expandApigeeInstanceDescription(d.Get("description"), d, config)
 	if err != nil {
 		return nil, err
@@ -100,6 +106,10 @@ func expandApigeeInstancePeeringCidrRange(v interface{}, d TerraformResourceData
 	return v, nil
 }
 
+func expandApigeeInstanceIpRange(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) {
+	return v, nil
+}
+
 func expandApigeeInstanceDescription(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) {
 	return v, nil
 }
diff --git a/converters/google/resources/getconfig.go b/converters/google/resources/getconfig.go
new file mode 100644
index 00000000..bd9fb3d5
--- /dev/null
+++ b/converters/google/resources/getconfig.go
@@ -0,0 +1,37 @@
+package google
+
+import (
+	"context"
+
+	"github.com/pkg/errors"
+)
+
+func GetConfig(ctx context.Context, project string, offline bool) (*Config, error) {
+	cfg := &Config{
+		Project: project,
+	}
+
+	// Search for default credentials
+	cfg.Credentials = multiEnvSearch([]string{
+		"GOOGLE_CREDENTIALS",
+		"GOOGLE_CLOUD_KEYFILE_JSON",
+		"GCLOUD_KEYFILE_JSON",
+	})
+
+	cfg.AccessToken = multiEnvSearch([]string{
+		"GOOGLE_OAUTH_ACCESS_TOKEN",
+	})
+
+	cfg.ImpersonateServiceAccount = multiEnvSearch([]string{
+		"GOOGLE_IMPERSONATE_SERVICE_ACCOUNT",
+	})
+
+	if !offline {
+		ConfigureBasePaths(cfg)
+		if err := cfg.LoadAndValidate(ctx); err != nil {
+			return nil, errors.Wrap(err, "load and validate config")
+		}
+	}
+
+	return cfg, nil
+}
diff --git a/converters/google/resources/resource_converters.go b/converters/google/resources/resource_converters.go
index 0a3a837c..7188d2d7 100644
--- a/converters/google/resources/resource_converters.go
+++ b/converters/google/resources/resource_converters.go
@@ -168,5 +168,6 @@ func ResourceConverters() map[string][]ResourceConverter {
 		"google_project_iam_member":           {resourceConverterProjectIamMember()},
 		"google_project_iam_custom_role":      {resourceConverterProjectIAMCustomRole()},
 		"google_organization_iam_custom_role": {resourceConverterOrganizationIAMCustomRole()},
+		"google_vpc_access_connector":         {resourceConverterVPCAccessConnector()},
 	}
 }
diff --git a/docs/supported_resources.md b/docs/supported_resources.md
index 8cdca85b..c702740a 100644
--- a/docs/supported_resources.md
+++ b/docs/supported_resources.md
@@ -5,140 +5,145 @@
 If you want terraform validator to add support for a resource, please [open an enhancement request](https://github.com/GoogleCloudPlatform/terraform-validator/issues/new?assignees=&labels=enhancement&template=enhancement.md) and consider [contributing code](./contributing/index.md).
 
 ```
-Terraform resource                               | CAI Asset Type
------------------------------------------------- | ------------------------------------------------
-google_access_context_manager_service_perimeter  | accesscontextmanager.googleapis.com/ServicePerimeter
-google_apigee_environment_iam_binding            | apigee.googleapis.com/Environment
-google_apigee_environment_iam_member             | apigee.googleapis.com/Environment
-google_apigee_environment_iam_policy             | apigee.googleapis.com/Environment
-google_bigquery_dataset                          | bigquery.googleapis.com/Dataset
-google_bigquery_dataset_iam_binding              | bigquery.googleapis.com/Dataset
-google_bigquery_dataset_iam_member               | bigquery.googleapis.com/Dataset
-google_bigquery_dataset_iam_policy               | bigquery.googleapis.com/Dataset
-google_bigquery_table                            | bigquery.googleapis.com/Table
-google_bigquery_table_iam_binding                | bigquery.googleapis.com/Table
-google_bigquery_table_iam_member                 | bigquery.googleapis.com/Table
-google_bigquery_table_iam_policy                 | bigquery.googleapis.com/Table
-google_bigtable_instance                         | bigtableadmin.googleapis.com/Cluster,bigtableadmin.googleapis.com/Instance
-google_binary_authorization_attestor_iam_binding | binaryauthorization.googleapis.com/Attestor
-google_binary_authorization_attestor_iam_member  | binaryauthorization.googleapis.com/Attestor
-google_binary_authorization_attestor_iam_policy  | binaryauthorization.googleapis.com/Attestor
-google_cloud_run_domain_mapping                  | run.googleapis.com/DomainMapping
-google_cloud_run_service                         | run.googleapis.com/Service
-google_cloudfunctions_function_iam_binding       | cloudfunctions.googleapis.com/CloudFunction
-google_cloudfunctions_function_iam_member        | cloudfunctions.googleapis.com/CloudFunction
-google_cloudfunctions_function_iam_policy        | cloudfunctions.googleapis.com/CloudFunction
-google_compute_address                           | compute.googleapis.com/Address
-google_compute_disk                              | compute.googleapis.com/Disk
-google_compute_disk_iam_binding                  | compute.googleapis.com/Disk
-google_compute_disk_iam_member                   | compute.googleapis.com/Disk
-google_compute_disk_iam_policy                   | compute.googleapis.com/Disk
-google_compute_firewall                          | compute.googleapis.com/Firewall
-google_compute_forwarding_rule                   | compute.googleapis.com/ForwardingRule
-google_compute_global_address                    | compute.googleapis.com/GlobalAddress
-google_compute_global_forwarding_rule            | compute.googleapis.com/GlobalForwardingRule
-google_compute_image_iam_binding                 | compute.googleapis.com/Image
-google_compute_image_iam_member                  | compute.googleapis.com/Image
-google_compute_image_iam_policy                  | compute.googleapis.com/Image
-google_compute_instance                          | compute.googleapis.com/Instance
-google_compute_instance_iam_binding              | compute.googleapis.com/Instance
-google_compute_instance_iam_member               | compute.googleapis.com/Instance
-google_compute_instance_iam_policy               | compute.googleapis.com/Instance
-google_compute_network                           | compute.googleapis.com/Network
-google_compute_region_disk_iam_binding           | compute.googleapis.com/RegionDisk
-google_compute_region_disk_iam_member            | compute.googleapis.com/RegionDisk
-google_compute_region_disk_iam_policy            | compute.googleapis.com/RegionDisk
-google_compute_security_policy                   | compute.googleapis.com/SecurityPolicy
-google_compute_snapshot                          | compute.googleapis.com/Snapshot
-google_compute_ssl_policy                        | compute.googleapis.com/SslPolicy
-google_compute_subnetwork                        | compute.googleapis.com/Subnetwork
-google_compute_subnetwork_iam_binding            | compute.googleapis.com/Subnetwork
-google_compute_subnetwork_iam_member             | compute.googleapis.com/Subnetwork
-google_compute_subnetwork_iam_policy             | compute.googleapis.com/Subnetwork
-google_container_cluster                         | container.googleapis.com/Cluster
-google_container_node_pool                       | container.googleapis.com/NodePool
-google_data_catalog_entry_group_iam_binding      | datacatalog.googleapis.com/EntryGroup
-google_data_catalog_entry_group_iam_member       | datacatalog.googleapis.com/EntryGroup
-google_data_catalog_entry_group_iam_policy       | datacatalog.googleapis.com/EntryGroup
-google_data_catalog_tag_template_iam_binding     | datacatalog.googleapis.com/TagTemplate
-google_data_catalog_tag_template_iam_member      | datacatalog.googleapis.com/TagTemplate
-google_data_catalog_tag_template_iam_policy      | datacatalog.googleapis.com/TagTemplate
-google_dns_managed_zone                          | dns.googleapis.com/ManagedZone
-google_dns_policy                                | dns.googleapis.com/Policy
-google_endpoints_service_iam_binding             | servicemanagement.googleapis.com/Service
-google_endpoints_service_iam_member              | servicemanagement.googleapis.com/Service
-google_endpoints_service_iam_policy              | servicemanagement.googleapis.com/Service
-google_filestore_instance                        | file.googleapis.com/Instance
-google_folder_iam_binding                        | cloudresourcemanager.googleapis.com/Folder
-google_folder_iam_member                         | cloudresourcemanager.googleapis.com/Folder
-google_folder_iam_policy                         | cloudresourcemanager.googleapis.com/Folder
-google_healthcare_consent_store_iam_binding      | healthcare.googleapis.com/ConsentStore
-google_healthcare_consent_store_iam_member       | healthcare.googleapis.com/ConsentStore
-google_healthcare_consent_store_iam_policy       | healthcare.googleapis.com/ConsentStore
-google_iap_tunnel_iam_binding                    | iap.googleapis.com/Tunnel
-google_iap_tunnel_iam_member                     | iap.googleapis.com/Tunnel
-google_iap_tunnel_iam_policy                     | iap.googleapis.com/Tunnel
-google_iap_tunnel_instance_iam_binding           | iap.googleapis.com/TunnelInstance
-google_iap_tunnel_instance_iam_member            | iap.googleapis.com/TunnelInstance
-google_iap_tunnel_instance_iam_policy            | iap.googleapis.com/TunnelInstance
-google_iap_web_iam_binding                       | iap.googleapis.com/Web
-google_iap_web_iam_member                        | iap.googleapis.com/Web
-google_iap_web_iam_policy                        | iap.googleapis.com/Web
-google_kms_crypto_key                            | cloudkms.googleapis.com/CryptoKey
-google_kms_crypto_key_iam_binding                | cloudkms.googleapis.com/CryptoKey
-google_kms_crypto_key_iam_member                 | cloudkms.googleapis.com/CryptoKey
-google_kms_crypto_key_iam_policy                 | cloudkms.googleapis.com/CryptoKey
-google_kms_key_ring                              | cloudkms.googleapis.com/KeyRing
-google_kms_key_ring_iam_binding                  | cloudkms.googleapis.com/KeyRing
-google_kms_key_ring_iam_member                   | cloudkms.googleapis.com/KeyRing
-google_kms_key_ring_iam_policy                   | cloudkms.googleapis.com/KeyRing
-google_monitoring_alert_policy                   | monitoring.googleapis.com/AlertPolicy
-google_monitoring_notification_channel           | monitoring.googleapis.com/NotificationChannel
-google_notebooks_instance_iam_binding            | notebooks.googleapis.com/Instance
-google_notebooks_instance_iam_member             | notebooks.googleapis.com/Instance
-google_notebooks_instance_iam_policy             | notebooks.googleapis.com/Instance
-google_notebooks_runtime_iam_binding             | notebooks.googleapis.com/Runtime
-google_notebooks_runtime_iam_member              | notebooks.googleapis.com/Runtime
-google_notebooks_runtime_iam_policy              | notebooks.googleapis.com/Runtime
-google_organization_iam_binding                  | cloudresourcemanager.googleapis.com/Organization
-google_organization_iam_custom_role              | iam.googleapis.com/Role
-google_organization_iam_member                   | cloudresourcemanager.googleapis.com/Organization
-google_organization_iam_policy                   | cloudresourcemanager.googleapis.com/Organization
-google_privateca_ca_pool_iam_binding             | privateca.googleapis.com/CaPool
-google_privateca_ca_pool_iam_member              | privateca.googleapis.com/CaPool
-google_privateca_ca_pool_iam_policy              | privateca.googleapis.com/CaPool
-google_project                                   | cloudbilling.googleapis.com/ProjectBillingInfo,cloudresourcemanager.googleapis.com/Project
-google_project_iam_binding                       | cloudresourcemanager.googleapis.com/Project
-google_project_iam_custom_role                   | iam.googleapis.com/Role
-google_project_iam_member                        | cloudresourcemanager.googleapis.com/Project
-google_project_iam_policy                        | cloudresourcemanager.googleapis.com/Project
-google_project_organization_policy               | cloudresourcemanager.googleapis.com/Project
-google_project_service                           | serviceusage.googleapis.com/Service
-google_pubsub_lite_topic                         | pubsublite.googleapis.com/Topic
-google_pubsub_schema                             | pubsub.googleapis.com/Schema
-google_pubsub_subscription                       | pubsub.googleapis.com/Subscription
-google_pubsub_subscription_iam_binding           | pubsub.googleapis.com/Subscription
-google_pubsub_subscription_iam_member            | pubsub.googleapis.com/Subscription
-google_pubsub_subscription_iam_policy            | pubsub.googleapis.com/Subscription
-google_pubsub_topic                              | pubsub.googleapis.com/Topic
-google_pubsub_topic_iam_binding                  | pubsub.googleapis.com/Topic
-google_pubsub_topic_iam_member                   | pubsub.googleapis.com/Topic
-google_pubsub_topic_iam_policy                   | pubsub.googleapis.com/Topic
-google_redis_instance                            | redis.googleapis.com/Instance
-google_secret_manager_secret_iam_binding         | secretmanager.googleapis.com/Secret
-google_secret_manager_secret_iam_member          | secretmanager.googleapis.com/Secret
-google_secret_manager_secret_iam_policy          | secretmanager.googleapis.com/Secret
-google_spanner_database                          | spanner.googleapis.com/Database
-google_spanner_database_iam_binding              | spanner.googleapis.com/Database
-google_spanner_database_iam_member               | spanner.googleapis.com/Database
-google_spanner_database_iam_policy               | spanner.googleapis.com/Database
-google_spanner_instance                          | spanner.googleapis.com/Instance
-google_spanner_instance_iam_binding              | spanner.googleapis.com/Instance
-google_spanner_instance_iam_member               | spanner.googleapis.com/Instance
-google_spanner_instance_iam_policy               | spanner.googleapis.com/Instance
-google_sql_database_instance                     | sqladmin.googleapis.com/Instance
-google_storage_bucket                            | storage.googleapis.com/Bucket
-google_storage_bucket_iam_binding                | storage.googleapis.com/Bucket
-google_storage_bucket_iam_member                 | storage.googleapis.com/Bucket
-google_storage_bucket_iam_policy                 | storage.googleapis.com/Bucket
+Terraform resource                                | CAI Asset Type
+------------------------------------------------- | ------------------------------------------------
+google_access_context_manager_service_perimeter   | accesscontextmanager.googleapis.com/ServicePerimeter
+google_apigee_environment_iam_binding             | apigee.googleapis.com/Environment
+google_apigee_environment_iam_member              | apigee.googleapis.com/Environment
+google_apigee_environment_iam_policy              | apigee.googleapis.com/Environment
+google_bigquery_dataset                           | bigquery.googleapis.com/Dataset
+google_bigquery_dataset_iam_binding               | bigquery.googleapis.com/Dataset
+google_bigquery_dataset_iam_member                | bigquery.googleapis.com/Dataset
+google_bigquery_dataset_iam_policy                | bigquery.googleapis.com/Dataset
+google_bigquery_table                             | bigquery.googleapis.com/Table
+google_bigquery_table_iam_binding                 | bigquery.googleapis.com/Table
+google_bigquery_table_iam_member                  | bigquery.googleapis.com/Table
+google_bigquery_table_iam_policy                  | bigquery.googleapis.com/Table
+google_bigtable_instance                          | bigtableadmin.googleapis.com/Cluster,bigtableadmin.googleapis.com/Instance
+google_binary_authorization_attestor_iam_binding  | binaryauthorization.googleapis.com/Attestor
+google_binary_authorization_attestor_iam_member   | binaryauthorization.googleapis.com/Attestor
+google_binary_authorization_attestor_iam_policy   | binaryauthorization.googleapis.com/Attestor
+google_cloud_run_domain_mapping                   | run.googleapis.com/DomainMapping
+google_cloud_run_service                          | run.googleapis.com/Service
+google_cloud_run_service_iam_binding              | run.googleapis.com/Service
+google_cloud_run_service_iam_member               | run.googleapis.com/Service
+google_cloud_run_service_iam_policy               | run.googleapis.com/Service
+google_cloudfunctions_function_iam_binding        | cloudfunctions.googleapis.com/CloudFunction
+google_cloudfunctions_function_iam_member         | cloudfunctions.googleapis.com/CloudFunction
+google_cloudfunctions_function_iam_policy         | cloudfunctions.googleapis.com/CloudFunction
+google_compute_address                            | compute.googleapis.com/Address
+google_compute_disk                               | compute.googleapis.com/Disk
+google_compute_disk_iam_binding                   | compute.googleapis.com/Disk
+google_compute_disk_iam_member                    | compute.googleapis.com/Disk
+google_compute_disk_iam_policy                    | compute.googleapis.com/Disk
+google_compute_firewall                           | compute.googleapis.com/Firewall
+google_compute_forwarding_rule                    | compute.googleapis.com/ForwardingRule
+google_compute_global_address                     | compute.googleapis.com/GlobalAddress
+google_compute_global_forwarding_rule             | compute.googleapis.com/GlobalForwardingRule
+google_compute_image_iam_binding                  | compute.googleapis.com/Image
+google_compute_image_iam_member                   | compute.googleapis.com/Image
+google_compute_image_iam_policy                   | compute.googleapis.com/Image
+google_compute_instance                           | compute.googleapis.com/Instance
+google_compute_instance_iam_binding               | compute.googleapis.com/Instance
+google_compute_instance_iam_member                | compute.googleapis.com/Instance
+google_compute_instance_iam_policy                | compute.googleapis.com/Instance
+google_compute_network                            | compute.googleapis.com/Network
+google_compute_region_disk_iam_binding            | compute.googleapis.com/RegionDisk
+google_compute_region_disk_iam_member             | compute.googleapis.com/RegionDisk
+google_compute_region_disk_iam_policy             | compute.googleapis.com/RegionDisk
+google_compute_security_policy                    | compute.googleapis.com/SecurityPolicy
+google_compute_snapshot                           | compute.googleapis.com/Snapshot
+google_compute_ssl_policy                         | compute.googleapis.com/SslPolicy
+google_compute_subnetwork                         | compute.googleapis.com/Subnetwork
+google_compute_subnetwork_iam_binding             | compute.googleapis.com/Subnetwork
+google_compute_subnetwork_iam_member              | compute.googleapis.com/Subnetwork
+google_compute_subnetwork_iam_policy              | compute.googleapis.com/Subnetwork
+google_container_cluster                          | container.googleapis.com/Cluster
+google_container_node_pool                        | container.googleapis.com/NodePool
+google_data_catalog_entry_group_iam_binding       | datacatalog.googleapis.com/EntryGroup
+google_data_catalog_entry_group_iam_member        | datacatalog.googleapis.com/EntryGroup
+google_data_catalog_entry_group_iam_policy        | datacatalog.googleapis.com/EntryGroup
+google_data_catalog_tag_template_iam_binding      | datacatalog.googleapis.com/TagTemplate
+google_data_catalog_tag_template_iam_member       | datacatalog.googleapis.com/TagTemplate
+google_data_catalog_tag_template_iam_policy       | datacatalog.googleapis.com/TagTemplate
+google_dns_managed_zone                           | dns.googleapis.com/ManagedZone
+google_dns_policy                                 | dns.googleapis.com/Policy
+google_endpoints_service_iam_binding              | servicemanagement.googleapis.com/Service
+google_endpoints_service_iam_member               | servicemanagement.googleapis.com/Service
+google_endpoints_service_iam_policy               | servicemanagement.googleapis.com/Service
+google_filestore_instance                         | file.googleapis.com/Instance
+google_folder_iam_binding                         | cloudresourcemanager.googleapis.com/Folder
+google_folder_iam_member                          | cloudresourcemanager.googleapis.com/Folder
+google_folder_iam_policy                          | cloudresourcemanager.googleapis.com/Folder
+google_healthcare_consent_store_iam_binding       | healthcare.googleapis.com/ConsentStore
+google_healthcare_consent_store_iam_member        | healthcare.googleapis.com/ConsentStore
+google_healthcare_consent_store_iam_policy        | healthcare.googleapis.com/ConsentStore
+google_iap_tunnel_iam_binding                     | iap.googleapis.com/Tunnel
+google_iap_tunnel_iam_member                      | iap.googleapis.com/Tunnel
+google_iap_tunnel_iam_policy                      | iap.googleapis.com/Tunnel
+google_iap_tunnel_instance_iam_binding            | iap.googleapis.com/TunnelInstance
+google_iap_tunnel_instance_iam_member             | iap.googleapis.com/TunnelInstance
+google_iap_tunnel_instance_iam_policy             | iap.googleapis.com/TunnelInstance
+google_iap_web_iam_binding                        | iap.googleapis.com/Web
+google_iap_web_iam_member                         | iap.googleapis.com/Web
+google_iap_web_iam_policy                         | iap.googleapis.com/Web
+google_kms_crypto_key                             | cloudkms.googleapis.com/CryptoKey
+google_kms_crypto_key_iam_binding                 | cloudkms.googleapis.com/CryptoKey
+google_kms_crypto_key_iam_member                  | cloudkms.googleapis.com/CryptoKey
+google_kms_crypto_key_iam_policy                  | cloudkms.googleapis.com/CryptoKey
+google_kms_key_ring                               | cloudkms.googleapis.com/KeyRing
+google_kms_key_ring_iam_binding                   | cloudkms.googleapis.com/KeyRing
+google_kms_key_ring_iam_member                    | cloudkms.googleapis.com/KeyRing
+google_kms_key_ring_iam_policy                    | cloudkms.googleapis.com/KeyRing
+google_monitoring_alert_policy                    | monitoring.googleapis.com/AlertPolicy
+google_monitoring_notification_channel            | monitoring.googleapis.com/NotificationChannel
+google_notebooks_instance_iam_binding             | notebooks.googleapis.com/Instance
+google_notebooks_instance_iam_member              | notebooks.googleapis.com/Instance
+google_notebooks_instance_iam_policy              | notebooks.googleapis.com/Instance
+google_notebooks_runtime_iam_binding              | notebooks.googleapis.com/Runtime
+google_notebooks_runtime_iam_member               | notebooks.googleapis.com/Runtime
+google_notebooks_runtime_iam_policy               | notebooks.googleapis.com/Runtime
+google_organization_iam_binding                   | cloudresourcemanager.googleapis.com/Organization
+google_organization_iam_custom_role               | iam.googleapis.com/Role
+google_organization_iam_member                    | cloudresourcemanager.googleapis.com/Organization
+google_organization_iam_policy                    | cloudresourcemanager.googleapis.com/Organization
+google_privateca_ca_pool_iam_binding              | privateca.googleapis.com/CaPool
+google_privateca_ca_pool_iam_member               | privateca.googleapis.com/CaPool
+google_privateca_ca_pool_iam_policy               | privateca.googleapis.com/CaPool
+google_project                                    | cloudbilling.googleapis.com/ProjectBillingInfo,cloudresourcemanager.googleapis.com/Project
+google_project_iam_binding                        | cloudresourcemanager.googleapis.com/Project
+google_project_iam_custom_role                    | iam.googleapis.com/Role
+google_project_iam_member                         | cloudresourcemanager.googleapis.com/Project
+google_project_iam_policy                         | cloudresourcemanager.googleapis.com/Project
+google_project_organization_policy                | cloudresourcemanager.googleapis.com/Project
+google_project_service                            | serviceusage.googleapis.com/Service
+google_pubsub_lite_reservation                    | pubsublite.googleapis.com/Reservation
+google_pubsub_lite_subscription                   | pubsublite.googleapis.com/Subscription
+google_pubsub_lite_topic                          | pubsublite.googleapis.com/Topic
+google_pubsub_schema                              | pubsub.googleapis.com/Schema
+google_pubsub_subscription                        | pubsub.googleapis.com/Subscription
+google_pubsub_subscription_iam_binding            | pubsub.googleapis.com/Subscription
+google_pubsub_subscription_iam_member             | pubsub.googleapis.com/Subscription
+google_pubsub_subscription_iam_policy             | pubsub.googleapis.com/Subscription
+google_pubsub_topic                               | pubsub.googleapis.com/Topic
+google_pubsub_topic_iam_binding                   | pubsub.googleapis.com/Topic
+google_pubsub_topic_iam_member                    | pubsub.googleapis.com/Topic
+google_pubsub_topic_iam_policy                    | pubsub.googleapis.com/Topic
+google_redis_instance                             | redis.googleapis.com/Instance
+google_secret_manager_secret_iam_binding          | secretmanager.googleapis.com/Secret
+google_secret_manager_secret_iam_member           | secretmanager.googleapis.com/Secret
+google_secret_manager_secret_iam_policy           | secretmanager.googleapis.com/Secret
+google_spanner_database                           | spanner.googleapis.com/Database
+google_spanner_database_iam_binding               | spanner.googleapis.com/Database
+google_spanner_database_iam_member                | spanner.googleapis.com/Database
+google_spanner_database_iam_policy                | spanner.googleapis.com/Database
+google_spanner_instance                           | spanner.googleapis.com/Instance
+google_spanner_instance_iam_binding               | spanner.googleapis.com/Instance
+google_spanner_instance_iam_member                | spanner.googleapis.com/Instance
+google_spanner_instance_iam_policy                | spanner.googleapis.com/Instance
+google_sql_database_instance                      | sqladmin.googleapis.com/Instance
+google_storage_bucket                             | storage.googleapis.com/Bucket
+google_storage_bucket_iam_binding                 | storage.googleapis.com/Bucket
+google_storage_bucket_iam_member                  | storage.googleapis.com/Bucket
+google_storage_bucket_iam_policy                  | storage.googleapis.com/Bucket
 ```


Test patch:
diff --git a/cmd/convert_test.go b/cmd/convert_test.go
index 2fa948a0..e1f3c75d 100644
--- a/cmd/convert_test.go
+++ b/cmd/convert_test.go
@@ -3,6 +3,8 @@ package cmd
 import (
 	"context"
 	"encoding/json"
+	"io/ioutil"
+	"path"
 	"testing"
 
 	"github.com/GoogleCloudPlatform/terraform-validator/converters/google"
@@ -109,3 +111,50 @@ func TestConvertRunLegacy(t *testing.T) {
 	a.Equal(errorJSON, "")
 	a.Equal(outputJSON, "")
 }
+
+func TestConvertRunOutputFile(t *testing.T) {
+	a := assert.New(t)
+	verbosity := "debug"
+	useStructuredLogging := false
+	errorLogger, errorBuf := newTestErrorLogger(verbosity, useStructuredLogging)
+	outputLogger, outputBuf := newTestOutputLogger()
+	ro := &rootOptions{
+		verbosity:            verbosity,
+		useStructuredLogging: useStructuredLogging,
+		errorLogger:          errorLogger,
+		outputLogger:         outputLogger,
+	}
+	outputPath := path.Join(t.TempDir(), "converted.json")
+	o := convertOptions{
+		project:           "",
+		ancestry:          "",
+		offline:           false,
+		rootOptions:       ro,
+		readPlannedAssets: MockReadPlannedAssets,
+		outputPath:        outputPath,
+	}
+
+	err := o.run("/path/to/plan")
+	a.Nil(err)
+
+	errorJSON := errorBuf.String()
+	outputJSON := outputBuf.String()
+
+	a.Equal(errorJSON, "")
+	a.Equal(outputJSON, "")
+
+	b, err := ioutil.ReadFile(outputPath)
+	if err != nil {
+		a.Failf("Unable to read file %s: %s", outputPath, err)
+	}
+	var gotAssets []interface{}
+	err = json.Unmarshal(b, &gotAssets)
+	if err != nil {
+		a.Failf("Failed to unmarshal file %s: %s", outputPath, err)
+	}
+
+	var expectedAssets []interface{}
+	expectedAssetJSON, _ := json.Marshal(testAssets())
+	json.Unmarshal(expectedAssetJSON, &expectedAssets)
+	a.Equal(expectedAssets, gotAssets)
+}
diff --git a/cmd/validate_test.go b/cmd/validate_test.go
index b6daf626..0d2941aa 100644
--- a/cmd/validate_test.go
+++ b/cmd/validate_test.go
@@ -3,10 +3,13 @@ package cmd
 import (
 	"context"
 	"encoding/json"
+	"io/ioutil"
+	"os"
+	"path"
 	"testing"
 
-	"github.com/GoogleCloudPlatform/terraform-validator/converters/google"
 	"github.com/GoogleCloudPlatform/config-validator/pkg/api/validator"
+	"github.com/GoogleCloudPlatform/terraform-validator/converters/google"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -57,7 +60,8 @@ func TestValidateRun(t *testing.T) {
 		validateAssets:    MockValidateAssetsWithViolations,
 	}
 
-	err := o.run("/path/to/plan")
+	inputPath := createEmptyFile(t, []byte{'0'})
+	err := o.run(inputPath)
 	a.ErrorIs(err, errViolations)
 
 	errorJSON := errorBuf.String()
@@ -102,7 +106,8 @@ func TestValidateRunNoViolations(t *testing.T) {
 		validateAssets:    MockValidateAssetsNoViolations,
 	}
 
-	err := o.run("/path/to/plan")
+	inputPath := createEmptyFile(t, []byte{'0'})
+	err := o.run(inputPath)
 	a.Nil(err)
 
 	errorJSON := errorBuf.String()
@@ -142,7 +147,8 @@ func TestValidateRunLegacy(t *testing.T) {
 		validateAssets:    MockValidateAssetsWithViolations,
 	}
 
-	err := o.run("/path/to/plan")
+	inputPath := createEmptyFile(t, []byte{'0'})
+	err := o.run(inputPath)
 	a.ErrorIs(err, errViolations)
 
 	errorJSON := errorBuf.String()
@@ -177,7 +183,47 @@ func TestValidateRunNoViolationsLegacy(t *testing.T) {
 		validateAssets:    MockValidateAssetsNoViolations,
 	}
 
-	err := o.run("/path/to/plan")
+	inputPath := createEmptyFile(t, []byte{'0'})
+	err := o.run(inputPath)
+	a.Nil(err)
+
+	errorJSON := errorBuf.String()
+	outputJSON := outputBuf.String()
+
+	// On a legacy run with no validation errors, loggers should not be used.
+	a.Equal("", errorJSON)
+	a.Equal("", outputJSON)
+}
+
+func TestValidateRunWithConvertedAssets(t *testing.T) {
+	a := assert.New(t)
+	verbosity := "debug"
+	useStructuredLogging := false
+	errorLogger, errorBuf := newTestErrorLogger(verbosity, useStructuredLogging)
+	outputLogger, outputBuf := newTestOutputLogger()
+	ro := &rootOptions{
+		verbosity:            verbosity,
+		useStructuredLogging: useStructuredLogging,
+		errorLogger:          errorLogger,
+		outputLogger:         outputLogger,
+	}
+	o := validateOptions{
+		project:        "",
+		ancestry:       "",
+		offline:        false,
+		policyPath:     "",
+		outputJSON:     false,
+		dryRun:         false,
+		rootOptions:    ro,
+		validateAssets: MockValidateAssetsNoViolations,
+	}
+
+	data, err := json.Marshal(testAssets())
+	if err != nil {
+		t.Fatalf("Failed to marshal assets: %s", err)
+	}
+	inputPath := createEmptyFile(t, data)
+	err = o.run(inputPath)
 	a.Nil(err)
 
 	errorJSON := errorBuf.String()
@@ -187,3 +233,11 @@ func TestValidateRunNoViolationsLegacy(t *testing.T) {
 	a.Equal("", errorJSON)
 	a.Equal("", outputJSON)
 }
+
+func createEmptyFile(t *testing.T, data []byte) string {
+	assetPath := path.Join(t.TempDir(), "testfile.json")
+	if err := ioutil.WriteFile(assetPath, data, os.ModePerm); err != nil {
+		t.Fatalf("Failed to write file %s: %s", assetPath, err)
+	}
+	return assetPath
+}
diff --git a/converters/google/resources/getconfig_test.go b/converters/google/resources/getconfig_test.go
new file mode 100644
index 00000000..662f2317
--- /dev/null
+++ b/converters/google/resources/getconfig_test.go
@@ -0,0 +1,92 @@
+package google
+
+import (
+	"context"
+	"os"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+type configAttrGetter func(cfg *Config) string
+
+func getCredentials(cfg *Config) string {
+	return cfg.Credentials
+}
+func getAccessToken(cfg *Config) string {
+	return cfg.AccessToken
+}
+func getImpersonateServiceAccount(cfg *Config) string {
+	return cfg.ImpersonateServiceAccount
+}
+
+func TestGetConfigExtractsEnvVars(t *testing.T) {
+	ctx := context.Background()
+	offline := true
+	cases := []struct {
+		name           string
+		envKey         string
+		envValue       string
+		getConfigValue configAttrGetter
+	}{
+		{
+			name:           "GOOGLE_CREDENTIALS",
+			envKey:         "GOOGLE_CREDENTIALS",
+			envValue:       "whatever",
+			getConfigValue: getCredentials,
+		},
+		{
+			name:           "GOOGLE_CLOUD_KEYFILE_JSON",
+			envKey:         "GOOGLE_CLOUD_KEYFILE_JSON",
+			envValue:       "whatever",
+			getConfigValue: getCredentials,
+		},
+		{
+			name:           "GCLOUD_KEYFILE_JSON",
+			envKey:         "GCLOUD_KEYFILE_JSON",
+			envValue:       "whatever",
+			getConfigValue: getCredentials,
+		},
+		{
+			name:           "GOOGLE_OAUTH_ACCESS_TOKEN",
+			envKey:         "GOOGLE_OAUTH_ACCESS_TOKEN",
+			envValue:       "whatever",
+			getConfigValue: getAccessToken,
+		},
+		{
+			name:           "GOOGLE_IMPERSONATE_SERVICE_ACCOUNT",
+			envKey:         "GOOGLE_IMPERSONATE_SERVICE_ACCOUNT",
+			envValue:       "whatever",
+			getConfigValue: getImpersonateServiceAccount,
+		},
+	}
+
+	for _, c := range cases {
+		t.Run(c.name, func(t *testing.T) {
+			originalValue, isSet := os.LookupEnv(c.envKey)
+			err := os.Setenv(c.envKey, c.envValue)
+			if err != nil {
+				t.Fatalf("error setting env var %s=%s: %s", c.envKey, c.envValue, err)
+			}
+
+			cfg, err := GetConfig(ctx, "project", offline)
+			if err != nil {
+				t.Fatalf("error building converter: %s", err)
+			}
+
+			assert.EqualValues(t, c.getConfigValue(cfg), c.envValue)
+
+			if isSet {
+				err = os.Setenv(c.envKey, originalValue)
+				if err != nil {
+					t.Fatalf("error setting env var %s=%s: %s", c.envKey, originalValue, err)
+				}
+			} else {
+				err = os.Unsetenv(c.envKey)
+				if err != nil {
+					t.Fatalf("error unsetting env var %s: %s", c.envKey, err)
+				}
+			}
+		})
+	}
+}
diff --git a/test/cli_test.go b/test/cli_test.go
index 79d2c5d3..e0a9c3c7 100644
--- a/test/cli_test.go
+++ b/test/cli_test.go
@@ -133,6 +133,7 @@ func TestCLI(t *testing.T) {
 		{name: "example_storage_bucket"},
 		{name: "example_storage_bucket_iam_member_random_suffix"},
 		{name: "example_storage_bucket_iam_policy"},
+		{name: "example_vpc_access_connector"},
 		{name: "full_compute_firewall"},
 		{name: "full_compute_instance"},
 		{name: "full_container_cluster"},
diff --git a/test/read_test.go b/test/read_test.go
index f88e0407..40c40c9c 100644
--- a/test/read_test.go
+++ b/test/read_test.go
@@ -97,6 +97,7 @@ func TestReadPlannedAssetsCoverage(t *testing.T) {
 		{name: "example_storage_bucket"},
 		{name: "example_storage_bucket_iam_member_random_suffix"},
 		{name: "example_storage_bucket_iam_policy"},
+		{name: "example_vpc_access_connector"},
 		{name: "full_compute_firewall"},
 		{name: "full_compute_instance"},
 		{name: "full_container_cluster"},
diff --git a/testdata/templates/example_vpc_access_connector.json b/testdata/templates/example_vpc_access_connector.json
new file mode 100644
index 00000000..0ad57536
--- /dev/null
+++ b/testdata/templates/example_vpc_access_connector.json
@@ -0,0 +1,19 @@
+[
+    {
+        "name": "//vpcaccess.googleapis.com/projects/{{.Provider.project}}/locations/us-central1/connectors/vpc-con",
+        "asset_type": "vpcaccess.googleapis.com/Connector",
+	"ancestry_path": "{{.Ancestry}}/project/{{.Provider.project}}",
+        "resource": {
+            "version": "v1",
+            "discovery_document_uri": "https://www.googleapis.com/discovery/v1/apis/vpcaccess/v1/rest",
+            "discovery_name": "Connector",
+	    "parent": "//cloudresourcemanager.googleapis.com/projects/{{.Provider.project}}",
+            "data": {
+                "ipCidrRange": "10.8.0.0/28",
+                "maxThroughput": 300,
+                "minThroughput": 200,
+                "network": "default"
+            }
+        }
+    }
+]
diff --git a/testdata/templates/example_vpc_access_connector.tf b/testdata/templates/example_vpc_access_connector.tf
new file mode 100644
index 00000000..8e2e45ec
--- /dev/null
+++ b/testdata/templates/example_vpc_access_connector.tf
@@ -0,0 +1,34 @@
+/**
+ * Copyright 2022 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.
+ */
+terraform {
+  required_providers {
+    google = {
+      source = "hashicorp/google"
+      version = "~> {{.Provider.version}}"
+    }
+  }
+}
+
+provider "google" {
+  {{if .Provider.credentials }}credentials = "{{.Provider.credentials}}"{{end}}
+}
+
+resource "google_vpc_access_connector" "connector" {
+  name          = "vpc-con"
+  ip_cidr_range = "10.8.0.0/28"
+  network       = "default"
+  region        = "us-central1"
+}
diff --git a/testdata/templates/example_vpc_access_connector.tfplan.json b/testdata/templates/example_vpc_access_connector.tfplan.json
new file mode 100644
index 00000000..e113206f
--- /dev/null
+++ b/testdata/templates/example_vpc_access_connector.tfplan.json
@@ -0,0 +1,85 @@
+{
+    "format_version": "0.1",
+    "terraform_version": "0.12.11",
+    "planned_values": {
+        "root_module": {
+            "resources": [
+                {
+                    "address": "google_vpc_access_connector.connector",
+                    "mode": "managed",
+                    "type": "google_vpc_access_connector",
+                    "name": "connector",
+                    "provider_name": "google",
+                    "schema_version": 0,
+                    "values": {
+                        "ip_cidr_range": "10.8.0.0/28",
+                        "max_throughput": 300,
+                        "min_throughput": 200,
+                        "name": "vpc-con",
+                        "network": "default",
+                        "region": "us-central1",
+                        "timeouts": null
+                    }
+                }
+            ]
+        }
+    },
+    "resource_changes": [
+        {
+            "address": "google_vpc_access_connector.connector",
+            "mode": "managed",
+            "type": "google_vpc_access_connector",
+            "name": "connector",
+            "provider_name": "google",
+            "change": {
+                "actions": [
+                    "create"
+                ],
+                "before": null,
+                "after": {
+                    "ip_cidr_range": "10.8.0.0/28",
+                    "max_throughput": 300,
+                    "min_throughput": 200,
+                    "name": "vpc-con",
+                    "network": "default",
+                    "region": "us-central1",
+                    "timeouts": null
+                },
+                "after_unknown": {
+                    "id": true,
+                    "project": true,
+                    "self_link": true,
+                    "state": true
+                }
+            }
+        }
+    ],
+    "configuration": {
+        "root_module": {
+            "resources": [
+                {
+                    "address": "google_vpc_access_connector.connector",
+                    "mode": "managed",
+                    "type": "google_vpc_access_connector",
+                    "name": "connector",
+                    "provider_config_key": "google",
+                    "expressions": {
+                        "ip_cidr_range": {
+                            "constant_value": "10.8.0.0/28"
+                        },
+                        "name": {
+                            "constant_value": "vpc-con"
+                        },
+                        "network": {
+                            "constant_value": "default"
+                        },
+                        "region": {
+                            "constant_value": "us-central1"
+                        }
+                    },
+                    "schema_version": 0
+                }
+            ]
+        }
+    }
+}