diff --git a/docs/tables/gcp_logging_bucket.md b/docs/tables/gcp_logging_bucket.md new file mode 100644 index 00000000..75695932 --- /dev/null +++ b/docs/tables/gcp_logging_bucket.md @@ -0,0 +1,29 @@ +# Table: gcp_logging_bucket + +Logging buckets store the logs that are routed from other projects, folders, or organizations. + +## Examples + +### Basic info + +```sql +select + name, + lifecycle_state, + description, + retention_days +from + gcp_logging_bucket; +``` + +### List locked buckets + +```sql +select + name, + locked +from + gcp_logging_bucket +where + locked; +``` diff --git a/gcp-test/tests/gcp_logging_bucket/dependencies.txt b/gcp-test/tests/gcp_logging_bucket/dependencies.txt new file mode 100644 index 00000000..e69de29b diff --git a/gcp-test/tests/gcp_logging_bucket/test-get-expected.json b/gcp-test/tests/gcp_logging_bucket/test-get-expected.json new file mode 100644 index 00000000..6b68e06b --- /dev/null +++ b/gcp-test/tests/gcp_logging_bucket/test-get-expected.json @@ -0,0 +1,8 @@ +[ + { + "location": "{{ output.region_id.value }}", + "name": "{{ resourceName }}", + "project": "{{ output.project_id.value }}", + "retention_days": 1 + } +] diff --git a/gcp-test/tests/gcp_logging_bucket/test-get-query.sql b/gcp-test/tests/gcp_logging_bucket/test-get-query.sql new file mode 100644 index 00000000..fa483a35 --- /dev/null +++ b/gcp-test/tests/gcp_logging_bucket/test-get-query.sql @@ -0,0 +1,3 @@ +select name, retention_days, location, project +from gcp.gcp_logging_bucket +where name = '{{ resourceName }}' and location = '{{ output.region_id.value }}'; \ No newline at end of file diff --git a/gcp-test/tests/gcp_logging_bucket/test-list-expected.json b/gcp-test/tests/gcp_logging_bucket/test-list-expected.json new file mode 100644 index 00000000..6a9606db --- /dev/null +++ b/gcp-test/tests/gcp_logging_bucket/test-list-expected.json @@ -0,0 +1,6 @@ +[ + { + "name": "{{ resourceName }}", + "title": "{{ resourceName }}" + } +] diff --git a/gcp-test/tests/gcp_logging_bucket/test-list-query.sql b/gcp-test/tests/gcp_logging_bucket/test-list-query.sql new file mode 100644 index 00000000..83d5812b --- /dev/null +++ b/gcp-test/tests/gcp_logging_bucket/test-list-query.sql @@ -0,0 +1,3 @@ +select name, title +from gcp.gcp_logging_bucket +where name = '{{ resourceName }}'; \ No newline at end of file diff --git a/gcp-test/tests/gcp_logging_bucket/test-not-found-expected.json b/gcp-test/tests/gcp_logging_bucket/test-not-found-expected.json new file mode 100644 index 00000000..19765bd5 --- /dev/null +++ b/gcp-test/tests/gcp_logging_bucket/test-not-found-expected.json @@ -0,0 +1 @@ +null diff --git a/gcp-test/tests/gcp_logging_bucket/test-not-found-query.sql b/gcp-test/tests/gcp_logging_bucket/test-not-found-query.sql new file mode 100644 index 00000000..d0835116 --- /dev/null +++ b/gcp-test/tests/gcp_logging_bucket/test-not-found-query.sql @@ -0,0 +1,3 @@ +select name, self_link, description +from gcp.gcp_logging_bucket +where name = 'dummy-{{resourceName}}' and location = '{{ output.region_id.value }}'; \ No newline at end of file diff --git a/gcp-test/tests/gcp_logging_bucket/test-turbot-expected.json b/gcp-test/tests/gcp_logging_bucket/test-turbot-expected.json new file mode 100644 index 00000000..ed9832a9 --- /dev/null +++ b/gcp-test/tests/gcp_logging_bucket/test-turbot-expected.json @@ -0,0 +1,8 @@ +[ + { + "akas": ["{{ output.resource_aka.value }}"], + "location": "{{ output.region_id.value }}", + "project": "{{ output.project_id.value }}", + "title": "{{ resourceName }}" + } +] diff --git a/gcp-test/tests/gcp_logging_bucket/test-turbot-query.sql b/gcp-test/tests/gcp_logging_bucket/test-turbot-query.sql new file mode 100644 index 00000000..653ac487 --- /dev/null +++ b/gcp-test/tests/gcp_logging_bucket/test-turbot-query.sql @@ -0,0 +1,3 @@ +select title, akas, location, project +from gcp.gcp_logging_bucket +where name = '{{ resourceName }}' and location = '{{ output.region_id.value }}'; \ No newline at end of file diff --git a/gcp-test/tests/gcp_logging_bucket/variables.json b/gcp-test/tests/gcp_logging_bucket/variables.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/gcp-test/tests/gcp_logging_bucket/variables.json @@ -0,0 +1 @@ +{} diff --git a/gcp-test/tests/gcp_logging_bucket/variables.tf b/gcp-test/tests/gcp_logging_bucket/variables.tf new file mode 100644 index 00000000..af198602 --- /dev/null +++ b/gcp-test/tests/gcp_logging_bucket/variables.tf @@ -0,0 +1,64 @@ + +variable "resource_name" { + type = string + default = "turbot-test-20200125-create-update" + description = "Name of the resource used throughout the test." +} + +variable "gcp_project" { + type = string + default = "niteowl-aaa" + description = "GCP project used for the test." +} + +variable "gcp_region" { + type = string + default = "us-east1" + description = "GCP region used for the test." +} + +variable "gcp_zone" { + type = string + default = "us-east1-b" +} + +provider "google" { + project = var.gcp_project + region = var.gcp_region + zone = var.gcp_zone +} + +data "google_client_config" "current" {} + +data "null_data_source" "resource" { + inputs = { + scope = "gcp://cloudresourcemanager.googleapis.com/projects/${data.google_client_config.current.project}" + } +} + +resource "google_logging_project_bucket_config" "named_test_resource" { + project = var.gcp_project + location = var.gcp_region + retention_days = 1 + bucket_id = var.resource_name +} + +output "resource_aka" { + value = "gcp://logging.googleapis.com/${google_logging_project_bucket_config.named_test_resource.id}" +} + +output "resource_id" { + value = google_logging_project_bucket_config.named_test_resource.id +} + +output "resource_name" { + value = var.resource_name +} + +output "project_id" { + value = var.gcp_project +} + +output "region_id" { + value = var.gcp_region +} diff --git a/gcp/plugin.go b/gcp/plugin.go index f783a0e6..a2e2911e 100644 --- a/gcp/plugin.go +++ b/gcp/plugin.go @@ -67,6 +67,7 @@ func Plugin(ctx context.Context) *plugin.Plugin { "gcp_iam_role": tableGcpIamRole(ctx), "gcp_kms_key": tableGcpKmsKey(ctx), "gcp_kms_key_ring": tableGcpKmsKeyRing(ctx), + "gcp_logging_bucket": tableGcpLoggingBucket(ctx), "gcp_logging_exclusion": tableGcpLoggingExclusion(ctx), "gcp_logging_metric": tableGcpLoggingMetric(ctx), "gcp_logging_sink": tableGcpLoggingSink(ctx), diff --git a/gcp/table_gcp_logging_bucket.go b/gcp/table_gcp_logging_bucket.go new file mode 100644 index 00000000..57bd51a0 --- /dev/null +++ b/gcp/table_gcp_logging_bucket.go @@ -0,0 +1,191 @@ +package gcp + +import ( + "context" + "strings" + + "github.com/turbot/steampipe-plugin-sdk/grpc/proto" + "github.com/turbot/steampipe-plugin-sdk/plugin" + "github.com/turbot/steampipe-plugin-sdk/plugin/transform" + + "google.golang.org/api/logging/v2" +) + +//// TABLE DEFINITION + +func tableGcpLoggingBucket(_ context.Context) *plugin.Table { + return &plugin.Table{ + Name: "gcp_logging_bucket", + Description: "GCP Logging Bucket", + Get: &plugin.GetConfig{ + KeyColumns: plugin.AllColumns([]string{"name", "location"}), + Hydrate: getLoggingBucket, + }, + List: &plugin.ListConfig{ + Hydrate: listLoggingBuckets, + }, + Columns: []*plugin.Column{ + { + Name: "name", + Description: "The resource name of the bucket.", + Type: proto.ColumnType_STRING, + Transform: transform.FromField("Name").Transform(lastPathElement), + }, + { + Name: "self_link", + Description: "The server-defined URL for the resource.", + Type: proto.ColumnType_STRING, + Transform: transform.FromP(loggingBucketTurbotData, "SelfLink"), + }, + { + Name: "create_time", + Description: "The creation timestamp of the bucket. This is not set for any of the default buckets.", + Type: proto.ColumnType_TIMESTAMP, + Transform: transform.FromGo().NullIfZero(), + }, + { + Name: "description", + Description: "Describes this bucket.", + Type: proto.ColumnType_STRING, + }, + { + Name: "lifecycle_state", + Description: "The bucket lifecycle state.", + Type: proto.ColumnType_STRING, + }, + { + Name: "locked", + Description: "Specifies whether the bucket has been locked, or not. The retention period on a locked bucket may not be changed. Locked buckets may only be deleted if they are empty.", + Type: proto.ColumnType_BOOL, + }, + { + Name: "retention_days", + Description: "Logs will be retained by default for this amount of time, after which they will automatically be deleted.", + Type: proto.ColumnType_INT, + }, + { + Name: "update_time", + Description: "The last update timestamp of the bucket.", + Type: proto.ColumnType_TIMESTAMP, + Transform: transform.FromGo().NullIfZero(), + }, + + // GCP standard columns + { + Name: "title", + Description: ColumnDescriptionTitle, + Type: proto.ColumnType_STRING, + Transform: transform.FromField("Name").Transform(lastPathElement), + }, + { + Name: "akas", + Description: ColumnDescriptionAkas, + Type: proto.ColumnType_JSON, + Transform: transform.FromP(loggingBucketTurbotData, "Akas"), + }, + + // GCP standard columns + { + Name: "location", + Description: ColumnDescriptionLocation, + Type: proto.ColumnType_STRING, + Transform: transform.FromP(loggingBucketTurbotData, "Location"), + }, + { + Name: "project", + Description: ColumnDescriptionProject, + Type: proto.ColumnType_STRING, + Transform: transform.FromP(loggingBucketTurbotData, "Project"), + }, + }, + } +} + +//// LIST FUNCTION + +func listLoggingBuckets(ctx context.Context, d *plugin.QueryData, _ *plugin.HydrateData) (interface{}, error) { + plugin.Logger(ctx).Trace("listLoggingBuckets") + + // Create service connection + service, err := LoggingService(ctx, d) + if err != nil { + return nil, err + } + + // Get project details + projectData, err := activeProject(ctx, d) + if err != nil { + return nil, err + } + + project := projectData.Project + + // '-' for all locations... + resp := service.Projects.Locations.Buckets.List("projects/" + project + "/locations/-") + if err := resp.Pages( + ctx, + func(page *logging.ListBucketsResponse) error { + for _, bucket := range page.Buckets { + d.StreamListItem(ctx, bucket) + } + return nil + }, + ); err != nil { + return nil, err + } + + return nil, err +} + +//// HYDRATE FUNCTIONS + +func getLoggingBucket(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) { + plugin.Logger(ctx).Trace("getLoggingBucket") + + // Create Service Connection + service, err := LoggingService(ctx, d) + if err != nil { + return nil, err + } + + bucketName := d.KeyColumnQuals["name"].GetStringValue() + locationId := d.KeyColumnQuals["location"].GetStringValue() + + // Return nil, if no input provided + if bucketName == "" || locationId == "" { + return nil, nil + } + + projectInfo, err := activeProject(ctx, d) + if err != nil { + return nil, err + } + bucketNameWithLocation := "projects/" + projectInfo.Project + "/locations/" + locationId + "/buckets/" + bucketName + + op, err := service.Projects.Locations.Buckets.Get(bucketNameWithLocation).Do() + if err != nil { + return nil, err + } + + return op, nil + +} + +//// TRANSFORM FUNCTIONS + +func loggingBucketTurbotData(_ context.Context, d *transform.TransformData) (interface{}, error) { + data := d.HydrateItem.(*logging.LogBucket) + param := d.Param.(string) + + // Fetch data from name + splittedTitle := strings.Split(data.Name, "/") + + turbotData := map[string]interface{}{ + "Project": splittedTitle[1], + "Location": splittedTitle[3], + "SelfLink": "https://logging.googleapis.com/v2/" + data.Name, + "Akas": []string{"gcp://logging.googleapis.com/" + data.Name}, + } + + return turbotData[param], nil +}