diff --git a/aws/plugin.go b/aws/plugin.go index c9ea62e0f..46d1732a0 100644 --- a/aws/plugin.go +++ b/aws/plugin.go @@ -179,6 +179,7 @@ func Plugin(ctx context.Context) *plugin.Plugin { "aws_cost_by_tag": tableAwsCostByTag(ctx), "aws_cost_forecast_daily": tableAwsCostForecastDaily(ctx), "aws_cost_forecast_monthly": tableAwsCostForecastMonthly(ctx), + "aws_costoptimizationhub_recommendation": tableAwsCostOptimizationHubRecommendation(ctx), "aws_cost_usage": tableAwsCostAndUsage(ctx), "aws_dax_cluster": tableAwsDaxCluster(ctx), "aws_dax_parameter": tableAwsDaxParameter(ctx), diff --git a/aws/service.go b/aws/service.go index 040916daa..f1b6a6dae 100644 --- a/aws/service.go +++ b/aws/service.go @@ -50,6 +50,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider" "github.com/aws/aws-sdk-go-v2/service/configservice" "github.com/aws/aws-sdk-go-v2/service/costexplorer" + "github.com/aws/aws-sdk-go-v2/service/costoptimizationhub" "github.com/aws/aws-sdk-go-v2/service/databasemigrationservice" "github.com/aws/aws-sdk-go-v2/service/dax" "github.com/aws/aws-sdk-go-v2/service/directoryservice" @@ -570,6 +571,15 @@ func CostExplorerClient(ctx context.Context, d *plugin.QueryData) (*costexplorer return costexplorer.NewFromConfig(*cfg), nil } +func CostOptimizationHubClient(ctx context.Context, d *plugin.QueryData) (*costoptimizationhub.Client, error) { + cfg, err := getClientForDefaultRegion(ctx, d) + if err != nil { + return nil, err + } + return costoptimizationhub.NewFromConfig(*cfg), nil +} + + func DatabaseMigrationClient(ctx context.Context, d *plugin.QueryData) (*databasemigrationservice.Client, error) { cfg, err := getClientForQueryRegion(ctx, d) if err != nil { diff --git a/aws/table_aws_costoptimizationhub_recommendation.go b/aws/table_aws_costoptimizationhub_recommendation.go new file mode 100644 index 000000000..a3464b407 --- /dev/null +++ b/aws/table_aws_costoptimizationhub_recommendation.go @@ -0,0 +1,350 @@ +package aws + +import ( + "context" + "fmt" + + "github.com/aws/aws-sdk-go-v2/service/costoptimizationhub" + "github.com/aws/aws-sdk-go-v2/service/costoptimizationhub/types" + + "github.com/turbot/steampipe-plugin-sdk/v5/grpc/proto" + "github.com/turbot/steampipe-plugin-sdk/v5/plugin" + "github.com/turbot/steampipe-plugin-sdk/v5/plugin/transform" +) + +func tableAwsCostOptimizationHubRecommendation(_ context.Context) *plugin.Table { + return &plugin.Table{ + Name: "aws_costoptimizationhub_recommendation", + Description: "AWS Cost Optimization Hub Recommendation", + List: &plugin.ListConfig{ + Hydrate: listCostOptimizationHubRecommendations, + KeyColumns: plugin.KeyColumnSlice{ + { + Name: "recommendation_account_id", + Require: plugin.Optional, + }, + { + Name: "action_type", + Require: plugin.Optional, + }, + { + Name: "implementation_effort", + Require: plugin.Optional, + }, + { + Name: "recommendation_id", + Require: plugin.Optional, + }, + { + Name: "resource_region", + Require: plugin.Optional, + }, + { + Name: "resource_arn", + Require: plugin.Optional, + }, + { + Name: "resource_id", + Require: plugin.Optional, + }, + { + Name: "current_resource_type", + Require: plugin.Optional, + }, + { + Name: "recommended_resource_type", + Require: plugin.Optional, + }, + { + Name: "restart_needed", + Require: plugin.Optional, + }, + { + Name: "rollback_possible", + Require: plugin.Optional, + }, + }, + Tags: map[string]string{"service": "cost-optimization-hub", "action": "ListRecommendations"}, + }, + Columns: awsGlobalRegionColumns( + []*plugin.Column{ + { + Name: "recommendation_id", + Description: "The ID for the recommendation.", + Type: proto.ColumnType_STRING, + }, + { + Name: "resource_arn", + Description: "The Amazon Resource Name (ARN) for the recommendation.", + Type: proto.ColumnType_STRING, + }, + { + Name: "resource_id", + Description: "The resource ID for the recommendation.", + Type: proto.ColumnType_STRING, + }, + + // We have a common column named "account_id" for all the tables that represents current caller account ID, so renamed it to "recommendation_account_id" to avoid ambiguity. + { + Name: "recommendation_account_id", + Description: "The account that the recommendation is for.", + Type: proto.ColumnType_STRING, + Transform: transform.FromField("AccountId"), + }, + { + Name: "action_type", + Description: "The type of tasks that can be carried out by this action.", + Type: proto.ColumnType_STRING, + }, + { + Name: "currency_code", + Description: "The currency code used for the recommendation.", + Type: proto.ColumnType_STRING, + }, + { + Name: "current_resource_summary", + Description: "Describes the current resource.", + Type: proto.ColumnType_STRING, + }, + { + Name: "current_resource_type", + Description: "The current resource type.", + Type: proto.ColumnType_STRING, + }, + { + Name: "estimated_monthly_cost", + Description: "The estimated monthly cost for the recommendation.", + Type: proto.ColumnType_DOUBLE, + }, + { + Name: "estimated_monthly_savings", + Description: "The estimated monthly savings amount for the recommendation.", + Type: proto.ColumnType_DOUBLE, + }, + { + Name: "estimated_savings_percentage", + Description: "The estimated savings percentage relative to the total cost over the cost calculation lookback period.", + Type: proto.ColumnType_DOUBLE, + }, + { + Name: "implementation_effort", + Description: "The effort required to implement the recommendation.", + Type: proto.ColumnType_STRING, + }, + { + Name: "last_refresh_timestamp", + Description: "The time when the recommendation was last generated.", + Type: proto.ColumnType_TIMESTAMP, + }, + { + Name: "recommendation_lookback_period_in_days", + Description: "The lookback period that's used to generate the recommendation.", + Type: proto.ColumnType_INT, + }, + { + Name: "recommended_resource_summary", + Description: "Describes the recommended resource.", + Type: proto.ColumnType_STRING, + }, + { + Name: "recommended_resource_type", + Description: "Describes the recommended resource.", + Type: proto.ColumnType_STRING, + }, + { + Name: "resource_region", + Description: "The Amazon Web Services Region of the resource.", + Type: proto.ColumnType_STRING, + Transform: transform.FromField("Region"), + }, + { + Name: "restart_needed", + Description: "Whether or not implementing the recommendation requires a restart.", + Type: proto.ColumnType_BOOL, + }, + { + Name: "rollback_possible", + Description: "Whether or not implementing the recommendation can be rolled back.", + Type: proto.ColumnType_BOOL, + }, + { + Name: "source", + Description: "The source of the recommendation.", + Type: proto.ColumnType_STRING, + }, + { + Name: "current_resource_details", + Description: "The details for the resource.", + Type: proto.ColumnType_JSON, + Hydrate: getCostOptimizationHubRecommendations, + Transform: transform.FromField("CurrentResourceDetails"), + }, + { + Name: "recommended_resource_details", + Description: "The details about the recommended resource.", + Type: proto.ColumnType_JSON, + Hydrate: getCostOptimizationHubRecommendations, + Transform: transform.FromField("RecommendedResourceDetails"), + }, + { + Name: "tags_src", + Description: "A list of tags assigned to the recommendation.", + Type: proto.ColumnType_JSON, + }, + + // Steampipe standard columns + { + Name: "tags", + Description: resourceInterfaceDescription("tags"), + Type: proto.ColumnType_JSON, + Transform: transform.From(costOptimizationRecommendationTurbotTags), + }, + { + Name: "title", + Description: resourceInterfaceDescription("title"), + Type: proto.ColumnType_STRING, + Transform: transform.FromField("RecommendationId"), + }, + }), + } +} + +//// LIST FUNCTION + +func listCostOptimizationHubRecommendations(ctx context.Context, d *plugin.QueryData, _ *plugin.HydrateData) (interface{}, error) { + params := buildCostOptimizationHubRecommendationInputFromQuals(d.Quals) + + // Create Client + svc, err := CostOptimizationHubClient(ctx, d) + if err != nil { + plugin.Logger(ctx).Error("aws_costoptimizationhub_recommendation.listCostOptimizationHubRecommendations", "connection_error", err) + return nil, err + } + + // Limiting the results + maxLimit := int32(1000) + if d.QueryContext.Limit != nil { + limit := int32(*d.QueryContext.Limit) + if limit < maxLimit { + maxLimit = limit + } + } + + input := &costoptimizationhub.ListRecommendationsInput{ + MaxResults: &maxLimit, + } + + if params != nil { + input.Filter = params + } + + paginator := costoptimizationhub.NewListRecommendationsPaginator(svc, input, func(o *costoptimizationhub.ListRecommendationsPaginatorOptions) { + o.Limit = maxLimit + o.StopOnDuplicateToken = true + }) + + for paginator.HasMorePages() { + d.WaitForListRateLimit(ctx) + + output, err := paginator.NextPage(ctx) + if err != nil { + plugin.Logger(ctx).Error("aws_costoptimizationhub_recommendation.listCostOptimizationHubRecommendations", "api_error", err) + return nil, err + } + + for _, item := range output.Items { + d.StreamListItem(ctx, item) + + // Context can be cancelled due to manual cancellation or the limit has been hit + if d.RowsRemaining(ctx) == 0 { + return nil, nil + } + } + } + + return nil, nil +} + +//// HYDRATE FUNCTIONS + +func getCostOptimizationHubRecommendations(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) { + recommendation := h.Item.(types.Recommendation) + + // Create Client + svc, err := CostOptimizationHubClient(ctx, d) + if err != nil { + plugin.Logger(ctx).Error("aws_costoptimizationhub_recommendation.getCostOptimizationHubRecommendations", "connection_error", err) + return nil, err + } + + input := &costoptimizationhub.GetRecommendationInput{ + RecommendationId: recommendation.RecommendationId, + } + + result, err := svc.GetRecommendation(ctx, input) + if err != nil { + plugin.Logger(ctx).Error("aws_costoptimizationhub_recommendation.getCostOptimizationHubRecommendations", "api_error", err) + return nil, err + } + + return result, nil +} + +//// TRANSFORM FUNCTIONS + +func costOptimizationRecommendationTurbotTags(_ context.Context, d *transform.TransformData) (interface{}, error) { + r := d.HydrateItem.(types.Recommendation) + var turbotTagsMap map[string]string + if r.Tags != nil { + turbotTagsMap = map[string]string{} + for _, i := range r.Tags { + turbotTagsMap[*i.Key] = *i.Value + } + } + return turbotTagsMap, nil +} + +//// Build input parameter fot list API call + +func buildCostOptimizationHubRecommendationInputFromQuals(quals plugin.KeyColumnQualMap) *types.Filter { + param := &types.Filter{} + filterQuals := []string{"recommendation_account_id", "action_type", "implementation_effort", "recommendation_id", "resource_region", "resource_arn", "resource_id", "current_resource_type", "recommended_resource_type", "restart_needed", "rollback_possible"} + + for _, columnName := range filterQuals { + if quals[columnName] != nil { + switch columnName { + case "restart_needed", "rollback_possible": + value := getQualsValueByColumn(quals, columnName, "boolean") + val := value.(string) == "true" + if columnName == "restart_needed" { + param.RestartNeeded = &val + } + if columnName == "rollback_possible" { + param.RollbackPossible = &val + } + default: + value := getQualsValueByColumn(quals, columnName, "string") + switch columnName { + case "recommendation_account_id": + param.AccountIds = []string{fmt.Sprint(value)} + case "recommendation_id": + param.RecommendationIds = []string{fmt.Sprint(value)} + case "resource_region": + param.Regions = []string{fmt.Sprint(value)} + case "resource_arn": + param.ResourceArns = []string{fmt.Sprint(value)} + case "resource_id": + param.ResourceIds = []string{fmt.Sprint(value)} + case "implementation_effort": + param.ImplementationEfforts = []types.ImplementationEffort{types.ImplementationEffort(value.(string))} + case "action_type": + param.ActionTypes = []types.ActionType{types.ActionType(value.(string))} + case "current_resource_type", "recommended_resource_type": + param.ResourceTypes = []types.ResourceType{types.ResourceType(value.(string))} + } + } + } + } + + return param +} diff --git a/docs/tables/aws_costoptimizationhub_recommendation.md b/docs/tables/aws_costoptimizationhub_recommendation.md new file mode 100644 index 000000000..ac60785e7 --- /dev/null +++ b/docs/tables/aws_costoptimizationhub_recommendation.md @@ -0,0 +1,184 @@ +--- +title: "Steampipe Table: aws_costoptimizationhub_recommendation - Query AWS Cost Optimization Recommendations using SQL" +description: "Allows users to query AWS Cost Optimization Hub Recommendations to obtain insights on cost-saving opportunities, resource configuration, and associated metadata." +--- + +# Table: aws_costoptimizationhub_recommendation - Query AWS Cost Optimization Recommendations using SQL + +The AWS Cost Optimization Hub provides recommendations for reducing costs and optimizing the usage of AWS resources. These recommendations are based on usage patterns, cost analysis, and resource configurations, helping organizations achieve better cost efficiency. + +## Table Usage Guide + +The `aws_costoptimizationhub_recommendation` table in Steampipe allows you to query detailed cost optimization recommendations. This table helps DevOps engineers, cost analysts, or financial professionals identify potential savings, understand implementation efforts, and track the effectiveness of recommendations. + +The schema outlines various attributes of the cost optimization recommendations, including estimated savings, resource details, recommendation types, and implementation efforts. It also provides timestamps for the last refresh and additional metadata such as tags and ARNs. + +**Important Notes** +- This table supports optional quals. Queries with optional quals are optimized to use additional filtering provided by the AWS API function to narrow down the results for better query performance.. Optional quals are supported for the following columns: + - `recommendation_account_id` + - `action_type` + - `implementation_effort` + - `recommendation_id` + - `resource_region` + - `resource_arn` + - `resource_id` + - `current_resource_type` + - `recommended_resource_type` + - `restart_needed` + - `rollback_possible` + +## Examples + +### Basic info +Retrieve the basic details of cost optimization recommendations, including the resource and estimated savings. + +```sql+postgres +select + recommendation_id, + resource_id, + estimated_monthly_savings, + estimated_savings_percentage, + implementation_effort +from + aws_costoptimizationhub_recommendation; +``` + +```sql+sqlite +select + recommendation_id, + resource_id, + estimated_monthly_savings, + estimated_savings_percentage, + implementation_effort +from + aws_costoptimizationhub_recommendation; +``` + +### List recommendations with significant savings +Identify recommendations where the estimated savings percentage is greater than 50%. This helps prioritize high-impact cost optimization opportunities. + +```sql+postgres +select + recommendation_id, + resource_id, + estimated_monthly_savings, + estimated_savings_percentage +from + aws_costoptimizationhub_recommendation +where + estimated_savings_percentage > 50; +``` + +```sql+sqlite +select + recommendation_id, + resource_id, + estimated_monthly_savings, + estimated_savings_percentage +from + aws_costoptimizationhub_recommendation +where + estimated_savings_percentage > 50; +``` + +### List recommendations requiring a resource restart +Find recommendations that require a restart to implement. This query helps in planning implementation efforts and minimizing downtime. + +```sql+postgres +select + recommendation_id, + resource_id, + implementation_effort, + restart_needed +from + aws_costoptimizationhub_recommendation +where + restart_needed = true; +``` + +```sql+sqlite +select + recommendation_id, + resource_id, + implementation_effort, + restart_needed +from + aws_costoptimizationhub_recommendation +where + restart_needed = 1; +``` + +### Get recommendations by resource type +Filter recommendations based on specific resource types, such as EC2 or RDS, to analyze opportunities for optimizing particular services. + +```sql+postgres +select + recommendation_id, + resource_id, + current_resource_type, + recommended_resource_type +from + aws_costoptimizationhub_recommendation +where + current_resource_type = 'EC2'; +``` + +```sql+sqlite +select + recommendation_id, + resource_id, + current_resource_type, + recommended_resource_type +from + aws_costoptimizationhub_recommendation +where + current_resource_type = 'EC2'; +``` + +### List recommendations refreshed in the last 30 days +Track recently updated recommendations to stay up-to-date with the latest cost optimization insights. + +```sql+postgres +select + recommendation_id, + resource_id, + last_refresh_timestamp +from + aws_costoptimizationhub_recommendation +where + last_refresh_timestamp > now() - interval '30 days'; +``` + +```sql+sqlite +select + recommendation_id, + resource_id, + last_refresh_timestamp +from + aws_costoptimizationhub_recommendation +where + last_refresh_timestamp > datetime('now','-30 days'); +``` + +### Get the tags associated with a recommendation +Retrieve tags assigned to recommendations to better organize and manage resources. + +```sql+postgres +select + recommendation_id, + resource_id, + jsonb_each_text(tags) as tag +from + aws_costoptimizationhub_recommendation; +``` + +```sql+sqlite +select + recommendation_id, + resource_id, + json_each(tags_src).key as tag_key, + json_each(tags_src).value as tag_value +from + aws_costoptimizationhub_recommendation; +``` + diff --git a/go.mod b/go.mod index 4f7ef1e83..afa6da6b2 100644 --- a/go.mod +++ b/go.mod @@ -43,6 +43,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider v1.36.3 github.com/aws/aws-sdk-go-v2/service/configservice v1.46.4 github.com/aws/aws-sdk-go-v2/service/costexplorer v1.37.1 + github.com/aws/aws-sdk-go-v2/service/costoptimizationhub v1.4.7 github.com/aws/aws-sdk-go-v2/service/databasemigrationservice v1.38.4 github.com/aws/aws-sdk-go-v2/service/dax v1.19.4 github.com/aws/aws-sdk-go-v2/service/directoryservice v1.24.4 @@ -118,6 +119,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/servicequotas v1.21.4 github.com/aws/aws-sdk-go-v2/service/ses v1.22.4 github.com/aws/aws-sdk-go-v2/service/sfn v1.26.4 + github.com/aws/aws-sdk-go-v2/service/shield v1.25.7 github.com/aws/aws-sdk-go-v2/service/simspaceweaver v1.10.4 github.com/aws/aws-sdk-go-v2/service/sns v1.29.4 github.com/aws/aws-sdk-go-v2/service/sqs v1.31.4 @@ -132,7 +134,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/wafv2 v1.48.2 github.com/aws/aws-sdk-go-v2/service/wellarchitected v1.29.4 github.com/aws/aws-sdk-go-v2/service/workspaces v1.38.4 - github.com/aws/smithy-go v1.20.4 + github.com/aws/smithy-go v1.22.1 github.com/gocarina/gocsv v0.0.0-20201208093247-67c824bc04d4 github.com/goccy/go-yaml v1.11.3 github.com/golang/protobuf v1.5.4 @@ -147,7 +149,6 @@ require ( require golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect require ( - github.com/aws/aws-sdk-go-v2/service/shield v1.25.7 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 // indirect diff --git a/go.sum b/go.sum index db85cbafe..23f0d205e 100644 --- a/go.sum +++ b/go.sum @@ -291,6 +291,8 @@ github.com/aws/aws-sdk-go-v2/service/configservice v1.46.4 h1:bmVA/LmysEu6gOplmz github.com/aws/aws-sdk-go-v2/service/configservice v1.46.4/go.mod h1:WCD4Psga99kZmdqPGJ88SURa6UMa4WgqpqzY5vP2ZS0= github.com/aws/aws-sdk-go-v2/service/costexplorer v1.37.1 h1:xjhk+io+kPtDOG5RizvHlkGKET3dxRBzorLdPPkpZQc= github.com/aws/aws-sdk-go-v2/service/costexplorer v1.37.1/go.mod h1:uLOg0o57AyQQhZGtUKIlcBJOKE53mO9bXKyrM9dFhy4= +github.com/aws/aws-sdk-go-v2/service/costoptimizationhub v1.4.7 h1:MGeK6VW2qK2jY5mG0a5VyJ9AFwxjQUumkZcUK/C1UDA= +github.com/aws/aws-sdk-go-v2/service/costoptimizationhub v1.4.7/go.mod h1:dD0mbm64tfE2DRlIVEKg0dXb9qyf+qZtNitsR8CvMVM= github.com/aws/aws-sdk-go-v2/service/databasemigrationservice v1.38.4 h1:ot9PKavvbeEg3eofQdkpJWrf8DR90S9wx1OirBUComU= github.com/aws/aws-sdk-go-v2/service/databasemigrationservice v1.38.4/go.mod h1:hTZS15Gghi40UxU03Cv09Qr2tXgoQrZOSGY6oaNUNAg= github.com/aws/aws-sdk-go-v2/service/dax v1.19.4 h1:S3mvtYjRVVsg1R4EuV1LWZUiD72t+pfnBbK8TL7zEmo= @@ -487,8 +489,8 @@ github.com/aws/aws-sdk-go-v2/service/wellarchitected v1.29.4 h1:OuFs453KXWTLBkem github.com/aws/aws-sdk-go-v2/service/wellarchitected v1.29.4/go.mod h1:MRT/P9Cwn+7xCCVpD1sTvUESiWMAc9hA+FooRsW5fe8= github.com/aws/aws-sdk-go-v2/service/workspaces v1.38.4 h1:SvHYikdxmnyptMebU3zFfXbfU96SHzdUX+KXqa6pjYE= github.com/aws/aws-sdk-go-v2/service/workspaces v1.38.4/go.mod h1:1XK49PATLHBd7mpKqO91GqRuV7bEsmyQ8Lslvn3fFj4= -github.com/aws/smithy-go v1.20.4 h1:2HK1zBdPgRbjFOHlfeQZfpC4r72MOb9bZkiFwggKO+4= -github.com/aws/smithy-go v1.20.4/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= +github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro= +github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=