diff --git a/pkg/iac/terraform/state/terraform_state_reader_test.go b/pkg/iac/terraform/state/terraform_state_reader_test.go index 28a3d591..33836122 100644 --- a/pkg/iac/terraform/state/terraform_state_reader_test.go +++ b/pkg/iac/terraform/state/terraform_state_reader_test.go @@ -349,6 +349,7 @@ func TestTerraformStateReader_Google_Resources(t *testing.T) { {name: "cloudfunctions function", dirName: "google_cloudfunctions_function", wantErr: false}, {name: "compute subnetwork", dirName: "google_compute_subnetwork", wantErr: false}, {name: "compute disk", dirName: "google_compute_disk", wantErr: false}, + {name: "compute image", dirName: "google_compute_image", wantErr: false}, {name: "bigtable instance", dirName: "google_bigtable_instance", wantErr: false}, {name: "bigtable table", dirName: "google_bigtable_table", wantErr: false}, {name: "sql database instance", dirName: "google_sql_database_instance", wantErr: false}, diff --git a/pkg/iac/terraform/state/test/google_compute_image/results.golden.json b/pkg/iac/terraform/state/test/google_compute_image/results.golden.json new file mode 100755 index 00000000..8cc75da4 --- /dev/null +++ b/pkg/iac/terraform/state/test/google_compute_image/results.golden.json @@ -0,0 +1,28 @@ +[ + { + "Id": "projects/cloudskiff-dev-elie/global/images/example-image", + "Type": "google_compute_image", + "Attrs": { + "archive_size_bytes": 539099200, + "creation_timestamp": "2021-10-27T07:18:35.424-07:00", + "description": "", + "disk_size_gb": 3, + "family": "", + "id": "projects/cloudskiff-dev-elie/global/images/example-image", + "label_fingerprint": "42WmSpB8rSM=", + "name": "example-image", + "project": "cloudskiff-dev-elie", + "raw_disk": [ + { + "container_type": "TAR", + "sha1": "", + "source": "https://storage.googleapis.com/bosh-gce-raw-stemcells/bosh-stemcell-97.98-google-kvm-ubuntu-xenial-go_agent-raw-1557960142.tar.gz" + } + ], + "self_link": "https://www.googleapis.com/compute/v1/projects/cloudskiff-dev-elie/global/images/example-image", + "source_disk": "", + "source_image": "", + "source_snapshot": "" + } + } +] \ No newline at end of file diff --git a/pkg/iac/terraform/state/test/google_compute_image/terraform.tfstate b/pkg/iac/terraform/state/test/google_compute_image/terraform.tfstate new file mode 100644 index 00000000..77613cb8 --- /dev/null +++ b/pkg/iac/terraform/state/test/google_compute_image/terraform.tfstate @@ -0,0 +1,48 @@ +{ + "version": 4, + "terraform_version": "0.15.5", + "serial": 92, + "lineage": "0738cef4-9d69-9ccc-aebd-1177cafa0aa9", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "google_compute_image", + "name": "example", + "provider": "provider[\"registry.terraform.io/hashicorp/google\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "archive_size_bytes": 539099200, + "creation_timestamp": "2021-10-27T07:18:35.424-07:00", + "description": "", + "disk_size_gb": 3, + "family": "", + "guest_os_features": [], + "id": "projects/cloudskiff-dev-elie/global/images/example-image", + "label_fingerprint": "42WmSpB8rSM=", + "labels": null, + "licenses": [], + "name": "example-image", + "project": "cloudskiff-dev-elie", + "raw_disk": [ + { + "container_type": "TAR", + "sha1": "", + "source": "https://storage.googleapis.com/bosh-gce-raw-stemcells/bosh-stemcell-97.98-google-kvm-ubuntu-xenial-go_agent-raw-1557960142.tar.gz" + } + ], + "self_link": "https://www.googleapis.com/compute/v1/projects/cloudskiff-dev-elie/global/images/example-image", + "source_disk": "", + "source_image": "", + "source_snapshot": "", + "timeouts": null + }, + "sensitive_attributes": [], + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozNjAwMDAwMDAwMDAsImRlbGV0ZSI6MzYwMDAwMDAwMDAwLCJ1cGRhdGUiOjM2MDAwMDAwMDAwMH19" + } + ] + } + ] +} diff --git a/pkg/remote/google/google_compute_image_enumerator.go b/pkg/remote/google/google_compute_image_enumerator.go new file mode 100644 index 00000000..aa73176e --- /dev/null +++ b/pkg/remote/google/google_compute_image_enumerator.go @@ -0,0 +1,49 @@ +package google + +import ( + remoteerror "github.com/cloudskiff/driftctl/pkg/remote/error" + "github.com/cloudskiff/driftctl/pkg/remote/google/repository" + "github.com/cloudskiff/driftctl/pkg/resource" + "github.com/cloudskiff/driftctl/pkg/resource/google" +) + +type GoogleComputeImageEnumerator struct { + repository repository.AssetRepository + factory resource.ResourceFactory +} + +func NewGoogleComputeImageEnumerator(repo repository.AssetRepository, factory resource.ResourceFactory) *GoogleComputeImageEnumerator { + return &GoogleComputeImageEnumerator{ + repository: repo, + factory: factory, + } +} + +func (e *GoogleComputeImageEnumerator) SupportedType() resource.ResourceType { + return google.GoogleComputeImageResourceType +} + +func (e *GoogleComputeImageEnumerator) Enumerate() ([]*resource.Resource, error) { + resources, err := e.repository.SearchAllImages() + + if err != nil { + return nil, remoteerror.NewResourceListingError(err, string(e.SupportedType())) + } + + results := make([]*resource.Resource, 0, len(resources)) + + for _, res := range resources { + results = append( + results, + e.factory.CreateAbstractResource( + string(e.SupportedType()), + trimResourceName(res.GetName()), + map[string]interface{}{ + "name": res.GetDisplayName(), + }, + ), + ) + } + + return results, err +} diff --git a/pkg/remote/google/init.go b/pkg/remote/google/init.go index 2bf7954a..3ceef278 100644 --- a/pkg/remote/google/init.go +++ b/pkg/remote/google/init.go @@ -91,6 +91,7 @@ func Init(version string, alerter *alerter.Alerter, remoteLibrary.AddEnumerator(NewGoogleComputeAddressEnumerator(assetRepository, factory)) remoteLibrary.AddEnumerator(NewGoogleCloudFunctionsFunctionEnumerator(assetRepository, factory)) remoteLibrary.AddEnumerator(NewGoogleComputeDiskEnumerator(assetRepository, factory)) + remoteLibrary.AddEnumerator(NewGoogleComputeImageEnumerator(assetRepository, factory)) remoteLibrary.AddEnumerator(NewGoogleBigTableInstanceEnumerator(assetRepository, factory)) remoteLibrary.AddEnumerator(NewGoogleBigtableTableEnumerator(assetRepository, factory)) remoteLibrary.AddEnumerator(NewGoogleSQLDatabaseInstanceEnumerator(assetRepository, factory)) diff --git a/pkg/remote/google/repository/asset.go b/pkg/remote/google/repository/asset.go index 96f245b2..760dbecf 100644 --- a/pkg/remote/google/repository/asset.go +++ b/pkg/remote/google/repository/asset.go @@ -20,6 +20,7 @@ const ( computeNetworkAssetType = "compute.googleapis.com/Network" computeSubnetworkAssetType = "compute.googleapis.com/Subnetwork" computeDiskAssetType = "compute.googleapis.com/Disk" + computeImageAssetType = "compute.googleapis.com/Image" dnsManagedZoneAssetType = "dns.googleapis.com/ManagedZone" computeInstanceGroupAssetType = "compute.googleapis.com/InstanceGroup" bigqueryDatasetAssetType = "bigquery.googleapis.com/Dataset" @@ -38,6 +39,7 @@ type AssetRepository interface { SearchAllInstances() ([]*assetpb.ResourceSearchResult, error) SearchAllNetworks() ([]*assetpb.ResourceSearchResult, error) SearchAllDisks() ([]*assetpb.ResourceSearchResult, error) + SearchAllImages() ([]*assetpb.ResourceSearchResult, error) SearchAllDNSManagedZones() ([]*assetpb.ResourceSearchResult, error) SearchAllInstanceGroups() ([]*assetpb.ResourceSearchResult, error) SearchAllDatasets() ([]*assetpb.ResourceSearchResult, error) @@ -125,6 +127,7 @@ func (s assetRepository) searchAllResources(ty string) ([]*assetpb.ResourceSearc bigqueryTableAssetType, computeAddressAssetType, computeDiskAssetType, + computeImageAssetType, }, } var results []*assetpb.ResourceSearchResult @@ -213,6 +216,10 @@ func (s assetRepository) SearchAllDisks() ([]*assetpb.ResourceSearchResult, erro return s.searchAllResources(computeDiskAssetType) } +func (s assetRepository) SearchAllImages() ([]*assetpb.ResourceSearchResult, error) { + return s.searchAllResources(computeImageAssetType) +} + func (s assetRepository) SearchAllBigtableInstances() ([]*assetpb.Asset, error) { return s.listAllResources(bigtableInstanceAssetType) } diff --git a/pkg/remote/google/repository/mock_AssetRepository.go b/pkg/remote/google/repository/mock_AssetRepository.go index f9c9c116..eb2c746a 100644 --- a/pkg/remote/google/repository/mock_AssetRepository.go +++ b/pkg/remote/google/repository/mock_AssetRepository.go @@ -219,6 +219,29 @@ func (_m *MockAssetRepository) SearchAllFunctions() ([]*asset.Asset, error) { return r0, r1 } +// SearchAllImages provides a mock function with given fields: +func (_m *MockAssetRepository) SearchAllImages() ([]*asset.ResourceSearchResult, error) { + ret := _m.Called() + + var r0 []*asset.ResourceSearchResult + if rf, ok := ret.Get(0).(func() []*asset.ResourceSearchResult); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*asset.ResourceSearchResult) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // SearchAllInstanceGroups provides a mock function with given fields: func (_m *MockAssetRepository) SearchAllInstanceGroups() ([]*asset.ResourceSearchResult, error) { ret := _m.Called() diff --git a/pkg/remote/google_compute_scanner_test.go b/pkg/remote/google_compute_scanner_test.go index 8af3fd90..4b94359c 100644 --- a/pkg/remote/google_compute_scanner_test.go +++ b/pkg/remote/google_compute_scanner_test.go @@ -973,3 +973,113 @@ func TestGoogleComputeDisk(t *testing.T) { }) } } + +func TestGoogleComputeImage(t *testing.T) { + + cases := []struct { + test string + assertExpected func(t *testing.T, got []*resource.Resource) + response []*assetpb.ResourceSearchResult + responseErr error + setupAlerterMock func(alerter *mocks.AlerterInterface) + wantErr error + }{ + { + test: "no compute image", + response: []*assetpb.ResourceSearchResult{}, + assertExpected: func(t *testing.T, got []*resource.Resource) { + assert.Len(t, got, 0) + }, + }, + { + test: "multiples images", + assertExpected: func(t *testing.T, got []*resource.Resource) { + assert.Len(t, got, 2) + assert.Equal(t, "projects/cloudskiff-dev-elie/global/images/example-image", got[0].ResourceId()) + assert.Equal(t, "google_compute_image", got[0].ResourceType()) + + assert.Equal(t, "projects/cloudskiff-dev-elie/global/images/example-image-2", got[1].ResourceId()) + assert.Equal(t, "google_compute_image", got[1].ResourceType()) + }, + response: []*assetpb.ResourceSearchResult{ + { + AssetType: "compute.googleapis.com/Image", + Name: "//compute.googleapis.com/projects/cloudskiff-dev-elie/global/images/example-image", + }, + { + AssetType: "compute.googleapis.com/Image", + Name: "//compute.googleapis.com/projects/cloudskiff-dev-elie/global/images/example-image-2", + }, + }, + }, + { + test: "cannot list images", + assertExpected: func(t *testing.T, got []*resource.Resource) { + assert.Len(t, got, 0) + }, + responseErr: status.Error(codes.PermissionDenied, "The caller does not have permission"), + setupAlerterMock: func(alerter *mocks.AlerterInterface) { + alerter.On( + "SendAlert", + "google_compute_image", + alerts.NewRemoteAccessDeniedAlert( + common.RemoteGoogleTerraform, + remoteerr.NewResourceListingError( + status.Error(codes.PermissionDenied, "The caller does not have permission"), + "google_compute_image", + ), + alerts.EnumerationPhase, + ), + ).Once() + }, + }, + } + + providerVersion := "3.78.0" + schemaRepository := testresource.InitFakeSchemaRepository("google", providerVersion) + googleresource.InitResourcesMetadata(schemaRepository) + factory := terraform.NewTerraformResourceFactory(schemaRepository) + + for _, c := range cases { + t.Run(c.test, func(tt *testing.T) { + scanOptions := ScannerOptions{} + providerLibrary := terraform.NewProviderLibrary() + remoteLibrary := common.NewRemoteLibrary() + + // Initialize mocks + alerter := &mocks.AlerterInterface{} + if c.setupAlerterMock != nil { + c.setupAlerterMock(alerter) + } + + assetClient, err := testgoogle.NewFakeAssetServer(c.response, c.responseErr) + if err != nil { + tt.Fatal(err) + } + + realProvider, err := terraform2.InitTestGoogleProvider(providerLibrary, providerVersion) + if err != nil { + tt.Fatal(err) + } + + repo := repository.NewAssetRepository(assetClient, realProvider.GetConfig(), cache.New(0)) + + remoteLibrary.AddEnumerator(google.NewGoogleComputeImageEnumerator(repo, factory)) + + testFilter := &filter.MockFilter{} + testFilter.On("IsTypeIgnored", mock.Anything).Return(false) + + s := NewScanner(remoteLibrary, alerter, scanOptions, testFilter) + got, err := s.Resources() + assert.Equal(tt, err, c.wantErr) + if err != nil { + return + } + alerter.AssertExpectations(tt) + testFilter.AssertExpectations(tt) + if c.assertExpected != nil { + c.assertExpected(t, got) + } + }) + } +} diff --git a/pkg/resource/google/google_compute_image.go b/pkg/resource/google/google_compute_image.go new file mode 100644 index 00000000..a4cbb4b8 --- /dev/null +++ b/pkg/resource/google/google_compute_image.go @@ -0,0 +1,13 @@ +package google + +import "github.com/cloudskiff/driftctl/pkg/resource" + +const GoogleComputeImageResourceType = "google_compute_image" + +func initGoogleComputeImageMetadata(resourceSchemaRepository resource.SchemaRepositoryInterface) { + resourceSchemaRepository.SetHumanReadableAttributesFunc(GoogleComputeImageResourceType, func(res *resource.Resource) map[string]string { + return map[string]string{ + "Name": *res.Attributes().GetString("name"), + } + }) +} diff --git a/pkg/resource/google/google_compute_image_test.go b/pkg/resource/google/google_compute_image_test.go new file mode 100644 index 00000000..87d7190c --- /dev/null +++ b/pkg/resource/google/google_compute_image_test.go @@ -0,0 +1,30 @@ +package google_test + +import ( + "testing" + + "github.com/cloudskiff/driftctl/test" + "github.com/cloudskiff/driftctl/test/acceptance" +) + +func TestAcc_Google_ComputeImage(t *testing.T) { + acceptance.Run(t, acceptance.AccTestCase{ + TerraformVersion: "0.15.5", + Paths: []string{"./testdata/acc/google_compute_image"}, + Args: []string{ + "scan", + "--to", "gcp+tf", + }, + Checks: []acceptance.AccCheck{ + { + Check: func(result *test.ScanResult, stdout string, err error) { + if err != nil { + t.Fatal(err) + } + result.AssertInfrastructureIsInSync() + result.AssertManagedCount(1) + }, + }, + }, + }) +} diff --git a/pkg/resource/google/metadatas.go b/pkg/resource/google/metadatas.go index 0116c4a5..72edfe92 100644 --- a/pkg/resource/google/metadatas.go +++ b/pkg/resource/google/metadatas.go @@ -15,4 +15,5 @@ func InitResourcesMetadata(resourceSchemaRepository resource.SchemaRepositoryInt initGoogleComputeAddressMetadata(resourceSchemaRepository) initGoogleComputeSubnetworkMetadata(resourceSchemaRepository) initGoogleComputeDiskMetadata(resourceSchemaRepository) + initGoogleComputeImageMetadata(resourceSchemaRepository) } diff --git a/pkg/resource/google/testdata/acc/google_compute_image/.driftignore b/pkg/resource/google/testdata/acc/google_compute_image/.driftignore new file mode 100644 index 00000000..aea4ffe2 --- /dev/null +++ b/pkg/resource/google/testdata/acc/google_compute_image/.driftignore @@ -0,0 +1,2 @@ +* +!google_compute_image diff --git a/pkg/resource/google/testdata/acc/google_compute_image/.terraform.lock.hcl b/pkg/resource/google/testdata/acc/google_compute_image/.terraform.lock.hcl new file mode 100644 index 00000000..9ba9d75b --- /dev/null +++ b/pkg/resource/google/testdata/acc/google_compute_image/.terraform.lock.hcl @@ -0,0 +1,21 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/google" { + version = "3.78.0" + constraints = "3.78.0" + hashes = [ + "h1:iCyTW8BWdr6Bvd5B89wkxlrB8xLxqHvT1CPmGuKembU=", + "zh:027971c4689b6130619827fe57ce260aaca060db3446817d3a92869dba7cc07f", + "zh:0876dbecc0d441bf2479edd17fe9141d77274b5071ea5f68ac26a2994bff66f3", + "zh:2a5363ed6b1b880f5284e604567cfdabecca809584c30bbe7f19ff568d1ea4cd", + "zh:2f5af69b70654bda91199f6393253e3e479107deebfeddc3fe5850b3a1e83dfb", + "zh:52e6816ef11f5f799a6626dfff384e2153b37450d8320f1ef1eee8f71a2a87b2", + "zh:59ae534607db13db35c0015c06d1ae6d4886f01f7e8fd4e07bc120236a01c494", + "zh:65ab2ed1746ea02d0b1bbd8a22ff3a95d09dc8bdb3841fbc17e45e9feccfb327", + "zh:877a71d24ff65ede3f0c5973168acfeaea0f2fea3757cab5600efcddfd3171d5", + "zh:8b10c9643a4a53148f6758bfd60804b33c2b838482f2c39ed210b729e6b1e2e8", + "zh:ba682648d9f6c11a6d04a250ac79eec39271f615f3ff60c5ae73ebfcc2cdb450", + "zh:e946561921e0279450e9b9f705de9354ce35562ed4cc0d4cd3512aa9eb1f6486", + ] +} diff --git a/pkg/resource/google/testdata/acc/google_compute_image/terraform.tf b/pkg/resource/google/testdata/acc/google_compute_image/terraform.tf new file mode 100644 index 00000000..8f34c829 --- /dev/null +++ b/pkg/resource/google/testdata/acc/google_compute_image/terraform.tf @@ -0,0 +1,18 @@ +provider "google" {} + +terraform { + required_version = "~> 0.15.0" + required_providers { + google = { + version = "3.78.0" + } + } +} + +resource "google_compute_image" "example" { + name = "example-image" + + raw_disk { + source = "https://storage.googleapis.com/bosh-gce-raw-stemcells/bosh-stemcell-97.98-google-kvm-ubuntu-xenial-go_agent-raw-1557960142.tar.gz" + } +} diff --git a/pkg/resource/resource_types.go b/pkg/resource/resource_types.go index a092bf70..d6499bc2 100644 --- a/pkg/resource/resource_types.go +++ b/pkg/resource/resource_types.go @@ -170,6 +170,7 @@ var supportedTypes = map[string]ResourceTypeMeta{ "google_bigtable_instance": {}, "google_bigtable_table": {}, "google_sql_database_instance": {}, + "google_compute_image": {}, "azurerm_storage_account": {}, "azurerm_storage_container": {},