Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for GCP scopes #1296

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
43 changes: 42 additions & 1 deletion pkg/cmd/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,22 @@ func NewScanCmd(opts *pkg.ScanOptions) *cobra.Command {
)
}

GCPScope, _ := cmd.Flags().GetStringSlice("gcp-scope")
limitScope := make([]string, 0)

if to == common.RemoteGoogleTerraform && len(GCPScope) > 0 {
limitScope, err = parseScopeFlag(GCPScope)
if err != nil {
return err
}
} else if to != common.RemoteGoogleTerraform && len(GCPScope) > 0 {
return errors.New("gcp-scope can only be utilized when using " + common.RemoteGoogleTerraform + " flag")
} else if to == common.RemoteGoogleTerraform && len(GCPScope) == 0 {
return errors.New("gcp-scope must be specified when using " + common.RemoteGoogleTerraform + " flag")
}
Comment on lines +73 to +77
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Plea try to avoid else-if statements, there is not reason do use them here


opts.GCPScope = limitScope

outputFlag, _ := cmd.Flags().GetStringSlice("output")

out, err := parseOutputFlags(outputFlag)
Expand Down Expand Up @@ -124,6 +140,11 @@ func NewScanCmd(opts *pkg.ScanOptions) *cobra.Command {
false,
"Do not display anything but scan results",
)
fl.StringSlice(
"gcp-scope",
[]string{},
"Set the GCP scope for search",
)
fl.StringArray(
"filter",
[]string{},
Expand Down Expand Up @@ -239,7 +260,7 @@ func scanRun(opts *pkg.ScanOptions) error {

resFactory := terraform.NewTerraformResourceFactory(resourceSchemaRepository)

err := remote.Activate(opts.To, opts.ProviderVersion, alerter, providerLibrary, remoteLibrary, scanProgress, resourceSchemaRepository, resFactory, opts.ConfigDir)
err := remote.Activate(opts.To, opts.ProviderVersion, opts.GCPScope, alerter, providerLibrary, remoteLibrary, scanProgress, resourceSchemaRepository, resFactory, opts.ConfigDir)
if err != nil {
return err
}
Expand Down Expand Up @@ -323,6 +344,26 @@ func scanRun(opts *pkg.ScanOptions) error {
return nil
}

func parseScopeFlag(scope []string) ([]string, error) {

scopeRegex := `projects/\S*$|folders/\d*$|organizations/\d*$`
r := regexp.MustCompile(scopeRegex)

for _, v := range scope {
if !r.MatchString(v) {
return nil, errors.Wrapf(
cmderrors.NewUsageError(
"\nAccepted formats are: projects/<project-id>, folders/<folder-number>, organizations/<org-id>",
),
"Unable to parse GCP scope '%s'",
v,
)
}
}

return scope, nil
}

func parseFromFlag(from []string) ([]config.SupplierConfig, error) {

configs := make([]config.SupplierConfig, 0, len(from))
Expand Down
4 changes: 2 additions & 2 deletions pkg/cmd/scan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -356,14 +356,14 @@ func Test_Options(t *testing.T) {
},
{
name: "should not find provider version in lockfile",
args: []string{"scan", "--to", "gcp+tf", "--tf-lockfile", "testdata/terraform_valid.lock.hcl"},
args: []string{"scan", "--to", "gcp+tf", "--gcp-scope", "organizations/123", "--tf-lockfile", "testdata/terraform_valid.lock.hcl"},
assertOptions: func(t *testing.T, opts *pkg.ScanOptions) {
assert.Equal(t, "", opts.ProviderVersion)
},
},
{
name: "should fail to read lockfile with silent error",
args: []string{"scan", "--to", "gcp+tf", "--tf-lockfile", "testdata/terraform_invalid.lock.hcl"},
args: []string{"scan", "--to", "gcp+tf", "--gcp-scope", "organizations/123", "--tf-lockfile", "testdata/terraform_invalid.lock.hcl"},
assertOptions: func(t *testing.T, opts *pkg.ScanOptions) {
assert.Equal(t, "", opts.ProviderVersion)
},
Expand Down
1 change: 1 addition & 0 deletions pkg/driftctl.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type ScanOptions struct {
Detect bool
From []config.SupplierConfig
To string
GCPScope []string
Output []output.OutputConfig
Filter *jmespath.JMESPath
Quiet bool
Expand Down
4 changes: 1 addition & 3 deletions pkg/remote/google/config/config.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package config

type GCPTerraformConfig struct {
Project string `cty:"project"`
Region string `cty:"region"`
Zone string `cty:"zone"`
Scopes []string `cty:"scopes"`
}
6 changes: 3 additions & 3 deletions pkg/remote/google/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (
"google.golang.org/api/cloudresourcemanager/v1"
)

func Init(version string, alerter *alerter.Alerter,
func Init(version string, gcpScope []string, alerter *alerter.Alerter,
providerLibrary *terraform.ProviderLibrary,
remoteLibrary *common.RemoteLibrary,
progress output.Progress,
Expand Down Expand Up @@ -51,9 +51,9 @@ func Init(version string, alerter *alerter.Alerter,
return err
}

assetRepository := repository.NewAssetRepository(assetClient, provider.GetConfig(), repositoryCache)
assetRepository := repository.NewAssetRepository(assetClient, provider.SetConfig(gcpScope), repositoryCache)
storageRepository := repository.NewStorageRepository(storageClient, repositoryCache)
iamRepository := repository.NewCloudResourceManagerRepository(crmService, provider.GetConfig(), repositoryCache)
iamRepository := repository.NewCloudResourceManagerRepository(crmService, provider.SetConfig(gcpScope), repositoryCache)

providerLibrary.AddProvider(terraform.GOOGLE, provider)
deserializer := resource.NewDeserializer(factory)
Expand Down
10 changes: 3 additions & 7 deletions pkg/remote/google/provider.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package google

import (
"os"

"github.com/snyk/driftctl/pkg/output"
"github.com/snyk/driftctl/pkg/remote/google/config"
"github.com/snyk/driftctl/pkg/remote/terraform"
Expand Down Expand Up @@ -34,7 +32,7 @@ func NewGCPTerraformProvider(version string, progress output.Progress, configDir
tfProvider, err := terraform.NewTerraformProvider(installer, terraform.TerraformProviderConfig{
Name: p.name,
GetProviderConfig: func(alias string) interface{} {
return p.GetConfig()
return p.SetConfig(nil)
},
}, progress)

Expand All @@ -55,10 +53,8 @@ func (p *GCPTerraformProvider) Version() string {
return p.version
}

func (p *GCPTerraformProvider) GetConfig() config.GCPTerraformConfig {
func (p *GCPTerraformProvider) SetConfig(scopes []string) config.GCPTerraformConfig {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetConfig was ok, we are not setting anything in this function

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well there's also nothing to get as during automated tests we have to set the config (scope)

return config.GCPTerraformConfig{
Project: os.Getenv("CLOUDSDK_CORE_PROJECT"),
Region: os.Getenv("CLOUDSDK_COMPUTE_REGION"),
Zone: os.Getenv("CLOUDSDK_COMPUTE_ZONE"),
Comment on lines -61 to -62
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you removed Region and Zone parameters ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because they are not needed since we use scopes.

Scopes: scopes,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you missed the point, scopes are not what we are expecting here in this struct https://registry.terraform.io/providers/hashicorp/google/latest/docs/guides/provider_reference#scopes

I think this PR is more complicated than you think, it seems that it is not possible to initialize a google terraform provider with a folder or organization scope (not to be confused with OAuth scopes). You can only configure a project.

We probably need to create another kind of detail fetcher that will lazy instantiate one terraform providers per project. We are doing that for S3 buckets, but it's a really tricky thing.

Did you try to run this PR in deep mode ? I think this is broken

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The entire point I had in mind was to support multiple or boarder scopes for scanning. I did not run it in deep mode since it's not recommended, but for a simple mode targeting various projects or an entire GCP org it works really good for me:

	"summary": {
		"total_resources": 400,
		"total_changed": 0,
		"total_unmanaged": 398,
		"total_missing": 1,
		"total_managed": 1
	}

}
}
198 changes: 122 additions & 76 deletions pkg/remote/google/repository/asset.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ package repository

import (
"context"
"errors"
"fmt"

asset "cloud.google.com/go/asset/apiv1"
"github.com/snyk/driftctl/pkg/remote/cache"
"github.com/snyk/driftctl/pkg/remote/google/config"
"google.golang.org/api/iterator"
assetpb "google.golang.org/genproto/googleapis/cloud/asset/v1"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

// https://cloud.google.com/asset-inventory/docs/supported-asset-types#supported_resource_types
Expand Down Expand Up @@ -75,102 +78,145 @@ func NewAssetRepository(client *asset.Client, config config.GCPTerraformConfig,
}

func (s assetRepository) listAllResources(ty string) ([]*assetpb.Asset, error) {
req := &assetpb.ListAssetsRequest{
Parent: fmt.Sprintf("projects/%s", s.config.Project),
ContentType: assetpb.ContentType_RESOURCE,
AssetTypes: []string{
cloudFunctionsFunction,
bigtableInstanceAssetType,
bigtableTableAssetType,
sqlDatabaseInstanceAssetType,
computeGlobalAddressAssetType,
nodeGroupAssetType,
},
}
var results []*assetpb.Asset

cacheKey := "listAllResources"
cachedResults := s.cache.GetAndLock(cacheKey)
defer s.cache.Unlock(cacheKey)
if cachedResults != nil {
results = cachedResults.([]*assetpb.Asset)
}
filteredResults := []*assetpb.Asset{}
var errorString string
var errCode codes.Code

for _, scope := range s.config.Scopes {
cacheKey := fmt.Sprintf("listAllResources_%s", scope)
cachedResults := s.cache.GetAndLock(cacheKey)
defer s.cache.Unlock(cacheKey)

req := &assetpb.ListAssetsRequest{
Parent: scope,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure that a folder is supported for listAssets call according to the documentation here : https://cloud.google.com/asset-inventory/docs/reference/rpc/google.cloud.asset.v1#google.cloud.asset.v1.ListAssetsRequest

Can you double check that ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed. Ideally we should not use listAssets but only searchAssets as the main difference is that list takes a snapshot of how the assets were at a particular time, which could result in a diff from what it's actually present.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where did you get this info ? I asked this question here a couple of month ago and I didn't get any answer.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From here there are a couple of hints about it:

Listing assets might not meet the performance requirements for large scale customers. If this is the case for you and calling list API encounters timeout, it's recommended to use export API instead.

Also in the API it's mentioned the purpose is to list resources from a specific timeframe

Lists assets with time and resource types and returns paged results in response.
readTime:  Timestamp to take an asset snapshot.  If not specified, the current time will be used. Due to delays in resource data collection and indexing, there is a volatile window during which running the same query may get different results.

While the search is more appropriate for our intent:

The Cloud Asset API allows you to use a custom query language to query resource metadata on a project, folder, or organization.
Method: searchAllResources. Searches all Cloud resources within the specified scope, such as a project, folder, or organization. 

ContentType: assetpb.ContentType_RESOURCE,
AssetTypes: []string{
cloudFunctionsFunction,
bigtableInstanceAssetType,
bigtableTableAssetType,
sqlDatabaseInstanceAssetType,
computeGlobalAddressAssetType,
nodeGroupAssetType,
},
}

var results []*assetpb.Asset

if cachedResults != nil {
results = cachedResults.([]*assetpb.Asset)
}

if results == nil {
it := s.client.ListAssets(context.Background(), req)
for {
resource, err := it.Next()
if err == iterator.Done {
break
if results == nil {
it := s.client.ListAssets(context.Background(), req)
for {
resource, err := it.Next()
if err == iterator.Done {
break
}
if err != nil && resource != nil {
errorString = errorString + fmt.Sprintf("For scope %s on resource %s got error: %s; ", scope, resource.AssetType, err.Error())
continue
}
if err != nil && resource == nil {
errorString = errorString + fmt.Sprintf("For scope %s got error: %s; ", scope, err.Error())
errCode = status.Code(err)
break
}
results = append(results, resource)
}
if err != nil {
return nil, err
s.cache.Put(cacheKey, results)
}

for _, result := range results {
if result.AssetType == ty {
filteredResults = append(filteredResults, result)
}
results = append(results, resource)
}
s.cache.Put(cacheKey, results)
}

filteredResults := []*assetpb.Asset{}
for _, result := range results {
if result.AssetType == ty {
filteredResults = append(filteredResults, result)
}
if len(errorString) > 0 && len(filteredResults) > 0 {
return filteredResults, errors.New(errorString)
}

if len(errorString) > 0 && len(filteredResults) == 0 {
return nil, status.Error(errCode, errorString)
}

return filteredResults, nil
}

func (s assetRepository) searchAllResources(ty string) ([]*assetpb.ResourceSearchResult, error) {
req := &assetpb.SearchAllResourcesRequest{
Scope: fmt.Sprintf("projects/%s", s.config.Project),
AssetTypes: []string{
storageBucketAssetType,
computeFirewallAssetType,
computeRouterAssetType,
computeInstanceAssetType,
computeNetworkAssetType,
computeSubnetworkAssetType,
dnsManagedZoneAssetType,
computeInstanceGroupAssetType,
bigqueryDatasetAssetType,
bigqueryTableAssetType,
computeAddressAssetType,
computeDiskAssetType,
computeImageAssetType,
healthCheckAssetType,
cloudRunServiceAssetType,
},
}
var results []*assetpb.ResourceSearchResult

cacheKey := "SearchAllResources"
cachedResults := s.cache.GetAndLock(cacheKey)
defer s.cache.Unlock(cacheKey)
if cachedResults != nil {
results = cachedResults.([]*assetpb.ResourceSearchResult)
}
filteredResults := []*assetpb.ResourceSearchResult{}
var errorString string
var errCode codes.Code

for _, scope := range s.config.Scopes {
cacheKey := fmt.Sprintf("SearchAllResources_%s", scope)
cachedResults := s.cache.GetAndLock(cacheKey)
defer s.cache.Unlock(cacheKey)

req := &assetpb.SearchAllResourcesRequest{
Scope: scope,
AssetTypes: []string{
storageBucketAssetType,
computeFirewallAssetType,
computeRouterAssetType,
computeInstanceAssetType,
computeNetworkAssetType,
computeSubnetworkAssetType,
dnsManagedZoneAssetType,
computeInstanceGroupAssetType,
bigqueryDatasetAssetType,
bigqueryTableAssetType,
computeAddressAssetType,
computeDiskAssetType,
computeImageAssetType,
healthCheckAssetType,
cloudRunServiceAssetType,
},
}
var results []*assetpb.ResourceSearchResult

if cachedResults != nil {
results = cachedResults.([]*assetpb.ResourceSearchResult)
}

if results == nil {
it := s.client.SearchAllResources(context.Background(), req)
for {
resource, err := it.Next()
if err == iterator.Done {
break
if results == nil {
it := s.client.SearchAllResources(context.Background(), req)
for {
resource, err := it.Next()
if err == iterator.Done {
break
}
if err != nil && resource != nil {
errorString = errorString + fmt.Sprintf("For scope %s on resource %s got error: %s; ", scope, resource.AssetType, err.Error())
continue
}
if err != nil && resource == nil {
errorString = errorString + fmt.Sprintf("For scope %s got error: %s; ", scope, err.Error())
errCode = status.Code(err)
break
}
results = append(results, resource)
}
if err != nil {
return nil, err
s.cache.Put(cacheKey, results)
}

for _, result := range results {
if result.AssetType == ty {
filteredResults = append(filteredResults, result)
}
results = append(results, resource)
}
s.cache.Put(cacheKey, results)
}

filteredResults := []*assetpb.ResourceSearchResult{}
for _, result := range results {
if result.AssetType == ty {
filteredResults = append(filteredResults, result)
}
if len(errorString) > 0 && len(filteredResults) > 0 {
return filteredResults, errors.New(errorString)
}

if len(errorString) > 0 && len(filteredResults) == 0 {
return nil, status.Error(errCode, errorString)
}

return filteredResults, nil
Expand Down