From b5be6cf94b86793a6b2fff2227da42e2aa28846a Mon Sep 17 00:00:00 2001 From: ParthaI Date: Wed, 29 Nov 2023 16:07:31 +0530 Subject: [PATCH 1/6] Added table azure_reservation_recommendation Closes #666 --- azure/plugin.go | 1 + ...e_azure_reserved_instance_recomendation.go | 206 ++++++++++++++++++ go.mod | 3 +- go.sum | 7 +- 4 files changed, 213 insertions(+), 4 deletions(-) create mode 100644 azure/table_azure_reserved_instance_recomendation.go diff --git a/azure/plugin.go b/azure/plugin.go index e32a9034..b0edea03 100644 --- a/azure/plugin.go +++ b/azure/plugin.go @@ -144,6 +144,7 @@ func Plugin(ctx context.Context) *plugin.Plugin { "azure_redis_cache": tableAzureRedisCache(ctx), "azure_resource_group": tableAzureResourceGroup(ctx), "azure_resource_link": tableAzureResourceLink(ctx), + "azure_reservation_recommendation": tableAzureReservationRecommendation(ctx), "azure_role_assignment": tableAzureIamRoleAssignment(ctx), "azure_role_definition": tableAzureIamRoleDefinition(ctx), "azure_route_table": tableAzureRouteTable(ctx), diff --git a/azure/table_azure_reserved_instance_recomendation.go b/azure/table_azure_reserved_instance_recomendation.go new file mode 100644 index 00000000..4aa86ffe --- /dev/null +++ b/azure/table_azure_reserved_instance_recomendation.go @@ -0,0 +1,206 @@ +package azure + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/services/consumption/mgmt/2019-10-01/consumption" + "github.com/turbot/steampipe-plugin-sdk/v5/grpc/proto" + "github.com/turbot/steampipe-plugin-sdk/v5/plugin/transform" + + "github.com/turbot/steampipe-plugin-sdk/v5/plugin" +) + +//// TABLE DEFINITION + +func tableAzureReservationRecommendation(ctx context.Context) *plugin.Table { + return &plugin.Table{ + Name: "azure_reservation_recommendation", + Description: "Azure Reservation Recommendation", + List: &plugin.ListConfig{ + Hydrate: listReservedInstanceRecomendations, + }, + Columns: azureColumns([]*plugin.Column{ + { + Name: "name", + Type: proto.ColumnType_STRING, + Description: "The ID that uniquely identifies an event.", + }, + { + Name: "id", + Description: "The full qualified ARM ID of an event.", + Type: proto.ColumnType_STRING, + Transform: transform.FromGo(), + }, + { + Name: "kind", + Description: "Specifies the kind of reservation recommendation.", + Type: proto.ColumnType_STRING, + }, + { + Name: "etag", + Description: "The etag for the resource.", + Type: proto.ColumnType_STRING, + }, + { + Name: "type", + Description: "Resource type.", + Type: proto.ColumnType_STRING, + }, + { + Name: "sku", + Description: "Resource sku.", + Type: proto.ColumnType_STRING, + }, + + // JSON fields + { + Name: "legacy_recommendation_properties", + Description: "The legacy recommendation properties.", + Type: proto.ColumnType_JSON, + }, + { + Name: "modern_recommendation_properties", + Description: "The legacy recommendation properties.", + Type: proto.ColumnType_JSON, + }, + + // Steampipe standard columns + { + Name: "title", + Description: ColumnDescriptionTitle, + Type: proto.ColumnType_STRING, + Transform: transform.FromField("Name"), + }, + { + Name: "tags", + Description: ColumnDescriptionTags, + Type: proto.ColumnType_JSON, + }, + { + Name: "akas", + Description: ColumnDescriptionAkas, + Type: proto.ColumnType_JSON, + Transform: transform.FromField("ID").Transform(idToAkas), + }, + + // Azure standard columns + { + Name: "region", + Description: ColumnDescriptionRegion, + Type: proto.ColumnType_STRING, + Transform: transform.FromField("Location").Transform(toLower), + }, + }), + } +} + +type RecomendationInfo struct { + LegacyRecommendationProperties *consumption.LegacyReservationRecommendationProperties + ModernRecommendationProperties *consumption.ModernReservationRecommendationProperties + ID *string + Name *string + Type *string + Etag *string + Tags map[string]*string + Location *string + Sku *string + Kind consumption.KindBasicReservationRecommendation +} + +//// LIST FUNCTION + +func listReservedInstanceRecomendations(ctx context.Context, d *plugin.QueryData, _ *plugin.HydrateData) (interface{}, error) { + session, err := GetNewSession(ctx, d, "MANAGEMENT") + if err != nil { + return nil, err + } + subscriptionID := session.SubscriptionID + + reservedInstanceClient := consumption.NewReservationRecommendationsClientWithBaseURI(session.ResourceManagerEndpoint, subscriptionID) + reservedInstanceClient.Authorizer = session.Authorizer + result, err := reservedInstanceClient.List(ctx, "subscriptions/"+subscriptionID, "") + if err != nil { + return nil, err + } + for _, recomendation := range result.Values() { + for _, r := range getReservationRecomendationProperties(recomendation) { + d.StreamListItem(ctx, r) + + // Check if context has been cancelled or if the limit has been hit (if specified) + // if there is a limit, it will return the number of rows required to reach this limit + if d.RowsRemaining(ctx) == 0 { + return nil, nil + } + } + } + + for result.NotDone() { + err = result.NextWithContext(ctx) + if err != nil { + return nil, err + } + for _, recomendation := range result.Values() { + for _, r := range getReservationRecomendationProperties(recomendation) { + d.StreamListItem(ctx, r) + + // Check if context has been cancelled or if the limit has been hit (if specified) + // if there is a limit, it will return the number of rows required to reach this limit + if d.RowsRemaining(ctx) == 0 { + return nil, nil + } + } + } + } + + return nil, err +} + +//// EXTRACT PROPERTIES + +func getReservationRecomendationProperties(data consumption.BasicReservationRecommendation) []*RecomendationInfo { + var results []*RecomendationInfo + lInfo, isLegacy := data.AsLegacyReservationRecommendation() + mInfo, isModern := data.AsModernReservationRecommendation() + info, is := data.AsReservationRecommendation() + if is { + result := &RecomendationInfo{} + result.Etag = info.Etag + result.ID = info.ID + result.Kind = info.Kind + result.Location = info.Location + result.Name = info.Name + result.Sku = info.Sku + result.Tags = info.Tags + result.Type = info.Type + results = append(results, result) + } + if isModern { + result := &RecomendationInfo{} + result.Etag = mInfo.Etag + result.ID = mInfo.ID + result.Kind = mInfo.Kind + result.Location = mInfo.Location + result.Name = mInfo.Name + result.Sku = mInfo.Sku + result.Tags = mInfo.Tags + result.Type = mInfo.Type + result.ModernRecommendationProperties = mInfo.ModernReservationRecommendationProperties + results = append(results, result) + } + + if isLegacy { + result := &RecomendationInfo{} + result.Etag = lInfo.Etag + result.ID = lInfo.ID + result.Kind = lInfo.Kind + result.Location = lInfo.Location + result.Name = lInfo.Name + result.Sku = lInfo.Sku + result.Tags = lInfo.Tags + result.Type = lInfo.Type + result.LegacyRecommendationProperties = lInfo.LegacyReservationRecommendationProperties + results = append(results, result) + } + + return results +} diff --git a/go.mod b/go.mod index 17fde63a..8546a14c 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.21 require ( github.com/Azure/azure-sdk-for-go v58.0.0+incompatible + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.1 github.com/Azure/azure-sdk-for-go/sdk/data/aztables v1.0.1 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/recoveryservices/armrecoveryservicesbackup/v3 v3.0.0 github.com/Azure/azure-storage-blob-go v0.12.0 @@ -24,7 +25,6 @@ require ( cloud.google.com/go/storage v1.30.1 // indirect github.com/Azure/azure-pipeline-go v0.2.3 // indirect github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1 // indirect - github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.1 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect github.com/Azure/go-autorest v14.2.0+incompatible // indirect github.com/Azure/go-autorest/autorest/adal v0.9.10 // indirect @@ -104,6 +104,7 @@ require ( github.com/prometheus/procfs v0.8.0 // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/sethvargo/go-retry v0.2.4 // indirect + github.com/shopspring/decimal v1.3.1 // indirect github.com/sirupsen/logrus v1.9.0 // indirect github.com/spf13/cast v1.5.0 // indirect github.com/stevenle/topsort v0.2.0 // indirect diff --git a/go.sum b/go.sum index 68b45d0b..28b398d8 100644 --- a/go.sum +++ b/go.sum @@ -314,8 +314,8 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cu github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8= github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= -github.com/dnaeon/go-vcr v1.1.0 h1:ReYa/UBrRyQdant9B4fNHGoCNKw6qh6P0fsdGmZpR7c= -github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= +github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= +github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= @@ -382,7 +382,6 @@ github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPh github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= -github.com/golang-jwt/jwt v3.2.1+incompatible h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfEP4Q1lOd9Z/+c= github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -664,6 +663,8 @@ github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sethvargo/go-retry v0.2.4 h1:T+jHEQy/zKJf5s95UkguisicE0zuF9y7+/vgz08Ocec= github.com/sethvargo/go-retry v0.2.4/go.mod h1:1afjQuvh7s4gflMObvjLPaWgluLLyhA1wmVZ6KLpICw= +github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= From f1aec796772388cc65bb641e5ed6dba585f0452e Mon Sep 17 00:00:00 2001 From: ParthaI Date: Wed, 29 Nov 2023 17:39:32 +0530 Subject: [PATCH 2/6] Added the optional quals for supported filter --- ...e_azure_reserved_instance_recomendation.go | 59 ++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/azure/table_azure_reserved_instance_recomendation.go b/azure/table_azure_reserved_instance_recomendation.go index 4aa86ffe..73839884 100644 --- a/azure/table_azure_reserved_instance_recomendation.go +++ b/azure/table_azure_reserved_instance_recomendation.go @@ -18,6 +18,11 @@ func tableAzureReservationRecommendation(ctx context.Context) *plugin.Table { Description: "Azure Reservation Recommendation", List: &plugin.ListConfig{ Hydrate: listReservedInstanceRecomendations, + KeyColumns: plugin.KeyColumnSlice{ + {Name: "look_back_period", Require: plugin.Optional, Operators: []string{"="}}, + {Name: "resource_type", Require: plugin.Optional, Operators: []string{"="}}, + {Name: "scope", Require: plugin.Optional, Operators: []string{"="}}, + }, }, Columns: azureColumns([]*plugin.Column{ { @@ -36,6 +41,27 @@ func tableAzureReservationRecommendation(ctx context.Context) *plugin.Table { Description: "Specifies the kind of reservation recommendation.", Type: proto.ColumnType_STRING, }, + { + Name: "look_back_period", + Description: "The number of days of usage to look back for recommendation. Allowed values Last7Days, Last30Days, Last60Days and default value is Last7Days.", + Type: proto.ColumnType_STRING, + Transform: transform.FromQual("look_back_period"), + Default: "Last7Days'", + }, + { + Name: "resource_type", + Description: "The type of resource for recommendation. Possible values are: VirtualMachines, SQLDatabases, PostgreSQL, ManagedDisk, MySQL, RedHat, MariaDB, RedisCache, CosmosDB, SqlDataWarehouse, SUSELinux, AppService, BlockBlob, AzureDataExplorer, VMwareCloudSimple and default value is VirtualMachines.", + Type: proto.ColumnType_STRING, + Transform: transform.FromQual("resource_type"), + Default: "VirtualMachines", + }, + { + Name: "scope", + Description: "Shared or single recommendation. allowed values 'Single' or 'Shared' and default value is Single.", + Type: proto.ColumnType_STRING, + Transform: transform.FromQual("scope"), + Default: "Single", + }, { Name: "etag", Description: "The etag for the resource.", @@ -118,7 +144,10 @@ func listReservedInstanceRecomendations(ctx context.Context, d *plugin.QueryData reservedInstanceClient := consumption.NewReservationRecommendationsClientWithBaseURI(session.ResourceManagerEndpoint, subscriptionID) reservedInstanceClient.Authorizer = session.Authorizer - result, err := reservedInstanceClient.List(ctx, "subscriptions/"+subscriptionID, "") + + filter := buildReservationRecomendationFilter(d.Quals) + + result, err := reservedInstanceClient.List(ctx, "subscriptions/"+subscriptionID, filter) if err != nil { return nil, err } @@ -204,3 +233,31 @@ func getReservationRecomendationProperties(data consumption.BasicReservationReco return results } + +//// BUILD INPUT FILTER FROM QUALS VALUE + +func buildReservationRecomendationFilter(quals plugin.KeyColumnQualMap) string { + filter := "" + + filterQuals := map[string]string{ + "look_back_period": "properties/lookBackPeriod", + "resource_type": "properties/resourceType", + "scope": "properties/scope", + } + + for columnName, filterName := range filterQuals { + if quals[columnName] != nil { + for _, q := range quals[columnName].Quals { + if q.Operator == "=" { + if filter == "" { + filter = filterName + " eq " + q.Value.GetStringValue() + } else { + filter += " AND " + filterName + " eq " + q.Value.GetStringValue() + } + } + } + } + } + + return filter +} From 8f6cbc5245b7c292f877458e17787cd4c7bc7e75 Mon Sep 17 00:00:00 2001 From: ParthaI Date: Wed, 29 Nov 2023 19:17:36 +0530 Subject: [PATCH 3/6] Updated the go file name as per table name and extracted the remmediation properties from the API response --- ...table_azure_reservation_recommendation.go} | 134 ++++++++++++++++-- 1 file changed, 123 insertions(+), 11 deletions(-) rename azure/{table_azure_reserved_instance_recomendation.go => table_azure_reservation_recommendation.go} (64%) diff --git a/azure/table_azure_reserved_instance_recomendation.go b/azure/table_azure_reservation_recommendation.go similarity index 64% rename from azure/table_azure_reserved_instance_recomendation.go rename to azure/table_azure_reservation_recommendation.go index 73839884..1e211ce6 100644 --- a/azure/table_azure_reserved_instance_recomendation.go +++ b/azure/table_azure_reservation_recommendation.go @@ -121,8 +121,8 @@ func tableAzureReservationRecommendation(ctx context.Context) *plugin.Table { } type RecomendationInfo struct { - LegacyRecommendationProperties *consumption.LegacyReservationRecommendationProperties - ModernRecommendationProperties *consumption.ModernReservationRecommendationProperties + LegacyRecommendationProperties map[string]interface{} + ModernRecommendationProperties map[string]interface{} ID *string Name *string Type *string @@ -213,7 +213,7 @@ func getReservationRecomendationProperties(data consumption.BasicReservationReco result.Sku = mInfo.Sku result.Tags = mInfo.Tags result.Type = mInfo.Type - result.ModernRecommendationProperties = mInfo.ModernReservationRecommendationProperties + result.ModernRecommendationProperties = extractRecomemendationProperties(mInfo.ModernReservationRecommendationProperties) results = append(results, result) } @@ -227,13 +227,125 @@ func getReservationRecomendationProperties(data consumption.BasicReservationReco result.Sku = lInfo.Sku result.Tags = lInfo.Tags result.Type = lInfo.Type - result.LegacyRecommendationProperties = lInfo.LegacyReservationRecommendationProperties + result.LegacyRecommendationProperties = extractRecomemendationProperties(lInfo.LegacyReservationRecommendationProperties) results = append(results, result) } return results } +func extractRecomemendationProperties(r interface{}) map[string]interface{} { + objectMap := make(map[string]interface{}) + switch item := r.(type) { + case *consumption.LegacyReservationRecommendationProperties: + if item != nil { + if item.LookBackPeriod != nil { + objectMap["LookBackPeriod"] = *item.LookBackPeriod + } + if item.InstanceFlexibilityRatio != nil { + objectMap["InstanceFlexibilityRatio"] = *item.InstanceFlexibilityRatio + } + if item.InstanceFlexibilityGroup != nil { + objectMap["InstanceFlexibilityGroup"] = *item.InstanceFlexibilityGroup + } + if item.NormalizedSize != nil { + objectMap["NormalizedSize"] = *item.NormalizedSize + } + if item.RecommendedQuantityNormalized != nil { + objectMap["RecommendedQuantityNormalized"] = *item.RecommendedQuantityNormalized + } + if item.MeterID != nil { + objectMap["MeterID"] = *item.MeterID + } + if item.ResourceType != nil { + objectMap["ResourceType"] = *item.ResourceType + } + if item.Term != nil { + objectMap["Term"] = *item.Term + } + if item.CostWithNoReservedInstances != nil { + objectMap["CostWithNoReservedInstances"] = *item.CostWithNoReservedInstances + } + if item.RecommendedQuantity != nil { + objectMap["RecommendedQuantity"] = *item.RecommendedQuantity + } + if item.TotalCostWithReservedInstances != nil { + objectMap["TotalCostWithReservedInstances"] = *item.TotalCostWithReservedInstances + } + if item.NetSavings != nil { + objectMap["NetSavings"] = *item.NetSavings + } + if item.FirstUsageDate != nil { + objectMap["FirstUsageDate"] = *item.FirstUsageDate + } + if item.Scope != nil { + objectMap["Scope"] = *item.Scope + } + if item.SkuProperties != nil { + objectMap["SkuProperties"] = *item.SkuProperties + } + } + case *consumption.ModernReservationRecommendationProperties: + if item != nil { + if item.Location != nil { + objectMap["Location"] = *item.Location + } + if item.LookBackPeriod != nil { + objectMap["LookBackPeriod"] = *item.LookBackPeriod + } + if item.InstanceFlexibilityRatio != nil { + objectMap["InstanceFlexibilityRatio"] = *item.InstanceFlexibilityRatio + } + if item.InstanceFlexibilityGroup != nil { + objectMap["InstanceFlexibilityGroup"] = *item.InstanceFlexibilityGroup + } + if item.NormalizedSize != nil { + objectMap["NormalizedSize"] = *item.NormalizedSize + } + if item.RecommendedQuantityNormalized != nil { + objectMap["RecommendedQuantityNormalized"] = *item.RecommendedQuantityNormalized + } + if item.MeterID != nil { + objectMap["MeterID"] = *item.MeterID + } + if item.Term != nil { + objectMap["Term"] = *item.Term + } + if item.CostWithNoReservedInstances != nil { + objectMap["CostWithNoReservedInstances"] = *item.CostWithNoReservedInstances + } + if item.RecommendedQuantity != nil { + objectMap["RecommendedQuantity"] = *item.RecommendedQuantity + } + if item.TotalCostWithReservedInstances != nil { + objectMap["TotalCostWithReservedInstances"] = *item.TotalCostWithReservedInstances + } + if item.NetSavings != nil { + objectMap["NetSavings"] = *item.NetSavings + } + if item.FirstUsageDate != nil { + objectMap["FirstUsageDate"] = *item.FirstUsageDate + } + if item.Scope != nil { + objectMap["Scope"] = *item.Scope + } + if item.SkuProperties != nil { + objectMap["SkuProperties"] = *item.SkuProperties + } + if item.SkuName != nil { + objectMap["SkuName"] = *item.SkuName + } + if item.ResourceType != nil { + objectMap["ResourceType"] = *item.ResourceType + } + if item.SubscriptionID != nil { + objectMap["SubscriptionID"] = *item.SubscriptionID + } + } + } + return objectMap +} + //// BUILD INPUT FILTER FROM QUALS VALUE func buildReservationRecomendationFilter(quals plugin.KeyColumnQualMap) string { @@ -247,16 +359,16 @@ func buildReservationRecomendationFilter(quals plugin.KeyColumnQualMap) string { for columnName, filterName := range filterQuals { if quals[columnName] != nil { - for _, q := range quals[columnName].Quals { - if q.Operator == "=" { - if filter == "" { - filter = filterName + " eq " + q.Value.GetStringValue() - } else { - filter += " AND " + filterName + " eq " + q.Value.GetStringValue() - } + for _, q := range quals[columnName].Quals { + if q.Operator == "=" { + if filter == "" { + filter = filterName + " eq " + q.Value.GetStringValue() + } else { + filter += " AND " + filterName + " eq " + q.Value.GetStringValue() } } } + } } return filter From 4a88ce008161f29499bcf5f6a291ca1e2a9d5ba6 Mon Sep 17 00:00:00 2001 From: ParthaI Date: Wed, 29 Nov 2023 19:51:02 +0530 Subject: [PATCH 4/6] Added the doc for the table --- .../azure_reservation_recommendation.md | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 docs/tables/azure_reservation_recommendation.md diff --git a/docs/tables/azure_reservation_recommendation.md b/docs/tables/azure_reservation_recommendation.md new file mode 100644 index 00000000..8966405d --- /dev/null +++ b/docs/tables/azure_reservation_recommendation.md @@ -0,0 +1,108 @@ +# Table: azure_reservation_recommendation + +Azure Reservations help you save money by committing to one-year or three-year plans for multiple products. Committing allows you to get a discount on the resources you use. Reservations can significantly reduce your resource costs by up to 72% from pay-as-you-go prices. Reservations provide a billing discount and don't affect the runtime state of your resources. After you purchase a reservation, the discount automatically applies to matching resources. + +**Note:** We can filter out the recommendations by using the columns `look_back_period`, `resource_type` or `scope` values in the query parameter. By default the table returns the data of resource type `VirtualMachines` with the scope `Single` for the last seven days of usage to look back for recommendation. + +## Examples + +### Basic info + +```sql +select + name, + id, + region, + scope, + etag, + type +from + azure_reservation_recommendation; +``` + +### Get reservation recommendation details for the last 30 days + +```sql +select + name, + tags, + sku, + look_back_period +from + azure_reservation_recommendation +where + look_back_period = 'Last30Days'; +``` + +### List reservation recommendation of the resource type MySQL + +```sql +select + name, + tags, + sku, + look_back_period, + resource_type +from + azure_reservation_recommendation +where + resource_type = 'MySQL'; +``` + +### Get legacy resrvation recommendation properties + +```sql +select + name, + id, + legacy_recommendation_properties ->> 'LookBackPeriod' as look_back_period, + legacy_recommendation_properties ->> 'InstanceFlexibilityRatio' as instance_flexibility_ratio, + legacy_recommendation_properties ->> 'InstanceFlexibilityGroup' as instance_flexibility_group, + legacy_recommendation_properties ->> 'NormalizedSize' as normalized_size, + legacy_recommendation_properties ->> 'RecommendedQuantityNormalized' as recommended_quantity_normalized, + legacy_recommendation_properties -> 'MeterID' as meter_id, + legacy_recommendation_properties ->> 'ResourceType' as resource_type, + legacy_recommendation_properties ->> 'Term' as term, + legacy_recommendation_properties -> 'CostWithNoReservedInstances' as cost_with_no_reserved_instances, + legacy_recommendation_properties -> 'RecommendedQuantity' as recommended_quantity, + legacy_recommendation_properties -> 'TotalCostWithReservedInstances' as total_cost_with_reserved_instances, + legacy_recommendation_properties -> 'NetSavings' as net_savings, + legacy_recommendation_properties ->> 'FirstUsageDate' as first_usage_date, + legacy_recommendation_properties ->> 'Scope' as scope, + legacy_recommendation_properties -> 'SkuProperties' as sku_properties +from + azure_reservation_recommendation +where + kind = 'legacy'; +``` + +### Get modern resrvation recommendation properties + +```sql +select + name, + id, + modern_recommendation_properties ->> 'Location' as location, + modern_recommendation_properties ->> 'LookBackPeriod' as look_back_period, + modern_recommendation_properties ->> 'InstanceFlexibilityRatio' as instance_flexibility_ratio, + modern_recommendation_properties ->> 'InstanceFlexibilityGroup' as instance_flexibility_group, + modern_recommendation_properties ->> 'NormalizedSize' as normalized_size, + modern_recommendation_properties ->> 'RecommendedQuantityNormalized' as recommended_quantity_normalized, + modern_recommendation_properties -> 'MeterID' as meter_id, + modern_recommendation_properties ->> 'ResourceType' as resource_type, + modern_recommendation_properties ->> 'Term' as term, + modern_recommendation_properties -> 'CostWithNoReservedInstances' as cost_with_no_reserved_instances, + modern_recommendation_properties -> 'RecommendedQuantity' as recommended_quantity, + modern_recommendation_properties -> 'TotalCostWithReservedInstances' as total_cost_with_reserved_instances, + modern_recommendation_properties -> 'NetSavings' as net_savings, + modern_recommendation_properties ->> 'FirstUsageDate' as first_usage_date, + modern_recommendation_properties ->> 'Scope' as scope, + modern_recommendation_properties -> 'SkuProperties' as sku_properties, + modern_recommendation_properties ->> 'SubscriptionID' as subscription_id, + modern_recommendation_properties ->> 'ResourceType' as resource_type, + modern_recommendation_properties ->> 'SkuName' as sku_name, +from + azure_reservation_recommendation +where + kind = 'modern'; +``` \ No newline at end of file From 50735ab64760e13a4a123b7c924f5851afd8c845 Mon Sep 17 00:00:00 2001 From: ParthaI Date: Wed, 29 Nov 2023 19:52:50 +0530 Subject: [PATCH 5/6] Fixed the example query with syntax error --- docs/tables/azure_reservation_recommendation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tables/azure_reservation_recommendation.md b/docs/tables/azure_reservation_recommendation.md index 8966405d..b5b9476b 100644 --- a/docs/tables/azure_reservation_recommendation.md +++ b/docs/tables/azure_reservation_recommendation.md @@ -100,7 +100,7 @@ select modern_recommendation_properties -> 'SkuProperties' as sku_properties, modern_recommendation_properties ->> 'SubscriptionID' as subscription_id, modern_recommendation_properties ->> 'ResourceType' as resource_type, - modern_recommendation_properties ->> 'SkuName' as sku_name, + modern_recommendation_properties ->> 'SkuName' as sku_name from azure_reservation_recommendation where From 0e644341fc0ea80c9ab9e8892a147857ec2f54ea Mon Sep 17 00:00:00 2001 From: ParthaI Date: Thu, 30 Nov 2023 17:06:33 +0530 Subject: [PATCH 6/6] Updated the filter string format --- azure/table_azure_reservation_recommendation.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/azure/table_azure_reservation_recommendation.go b/azure/table_azure_reservation_recommendation.go index 1e211ce6..385b80ba 100644 --- a/azure/table_azure_reservation_recommendation.go +++ b/azure/table_azure_reservation_recommendation.go @@ -3,7 +3,7 @@ package azure import ( "context" - "github.com/Azure/azure-sdk-for-go/services/consumption/mgmt/2019-10-01/consumption" + "github.com/Azure/azure-sdk-for-go/profiles/latest/consumption/mgmt/consumption" "github.com/turbot/steampipe-plugin-sdk/v5/grpc/proto" "github.com/turbot/steampipe-plugin-sdk/v5/plugin/transform" @@ -145,9 +145,11 @@ func listReservedInstanceRecomendations(ctx context.Context, d *plugin.QueryData reservedInstanceClient := consumption.NewReservationRecommendationsClientWithBaseURI(session.ResourceManagerEndpoint, subscriptionID) reservedInstanceClient.Authorizer = session.Authorizer + // E.g: properties/scope eq 'Single' AND properties/lookBackPeriod eq 'Last7Days' AND properties/resourceType eq 'VirtualMachines'" filter := buildReservationRecomendationFilter(d.Quals) - result, err := reservedInstanceClient.List(ctx, "subscriptions/"+subscriptionID, filter) + recommendationScope := "/subscriptions/"+subscriptionID+"/" + result, err := reservedInstanceClient.List(ctx, recommendationScope, filter) if err != nil { return nil, err } @@ -362,9 +364,9 @@ func buildReservationRecomendationFilter(quals plugin.KeyColumnQualMap) string { for _, q := range quals[columnName].Quals { if q.Operator == "=" { if filter == "" { - filter = filterName + " eq " + q.Value.GetStringValue() + filter = filterName + " eq " + "'"+q.Value.GetStringValue()+"'" } else { - filter += " AND " + filterName + " eq " + q.Value.GetStringValue() + filter += " AND " + filterName + " eq " + "'"+q.Value.GetStringValue()+"'" } } }