diff --git a/internal/providers/terraform/ibm/database.go b/internal/providers/terraform/ibm/database.go new file mode 100644 index 00000000000..8a91403a2b5 --- /dev/null +++ b/internal/providers/terraform/ibm/database.go @@ -0,0 +1,39 @@ +package ibm + +import ( + "github.com/infracost/infracost/internal/resources/ibm" + "github.com/infracost/infracost/internal/schema" +) + +func getDatabaseRegistryItem() *schema.RegistryItem { + return &schema.RegistryItem{ + Name: "ibm_database", + RFunc: newDatabase, + } +} + +func newDatabase(d *schema.ResourceData, u *schema.UsageData) *schema.Resource { + plan := d.Get("plan").String() + location := d.Get("location").String() + service := d.Get("service").String() + name := d.Get("name").String() + + r := &ibm.Database{ + Name: name, + Address: d.Address, + Service: service, + Plan: plan, + Location: location, + Group: d.RawValues, + } + r.PopulateUsage(u) + + configuration := make(map[string]any) + configuration["service"] = service + configuration["plan"] = plan + configuration["location"] = location + + SetCatalogMetadata(d, service, configuration) + + return r.BuildResource() +} diff --git a/internal/providers/terraform/ibm/database_test.go b/internal/providers/terraform/ibm/database_test.go new file mode 100644 index 00000000000..66442a0da1c --- /dev/null +++ b/internal/providers/terraform/ibm/database_test.go @@ -0,0 +1,16 @@ +package ibm_test + +import ( + "testing" + + "github.com/infracost/infracost/internal/providers/terraform/tftest" +) + +func TestDatabase(t *testing.T) { + t.Parallel() + if testing.Short() { + t.Skip("skipping test in short mode") + } + + tftest.GoldenFileResourceTests(t, "database_test") +} diff --git a/internal/providers/terraform/ibm/registry.go b/internal/providers/terraform/ibm/registry.go index 5c92aa74875..0a399285ef7 100644 --- a/internal/providers/terraform/ibm/registry.go +++ b/internal/providers/terraform/ibm/registry.go @@ -19,6 +19,7 @@ var ResourceRegistry []*schema.RegistryItem = []*schema.RegistryItem{ getIsLbRegistryItem(), getIsPublicGatewayRegistryItem(), getIbmPiVolumeRegistryItem(), + getDatabaseRegistryItem(), } // FreeResources grouped alphabetically diff --git a/internal/providers/terraform/ibm/testdata/database_test/database_test.golden b/internal/providers/terraform/ibm/testdata/database_test/database_test.golden new file mode 100644 index 00000000000..294f223d09f --- /dev/null +++ b/internal/providers/terraform/ibm/testdata/database_test/database_test.golden @@ -0,0 +1,16 @@ + + Name Monthly Qty Unit Monthly Cost + + ibm_database.test_db1 + ├─ RAM 24 GB-RAM $129.36 + ├─ Disk 256 GB-DISK $161.28 + └─ Core 6 Virtual Processor Core $193.80 + + ibm_database.test_db2 + ├─ RAM 60 GB-RAM $323.40 + └─ Disk 20 GB-DISK $12.60 + + OVERALL TOTAL $820.44 +────────────────────────────────── +2 cloud resources were detected: +∙ 2 were estimated \ No newline at end of file diff --git a/internal/providers/terraform/ibm/testdata/database_test/database_test.tf b/internal/providers/terraform/ibm/testdata/database_test/database_test.tf new file mode 100644 index 00000000000..7b5eb863268 --- /dev/null +++ b/internal/providers/terraform/ibm/testdata/database_test/database_test.tf @@ -0,0 +1,50 @@ + +terraform { + required_providers { + ibm = { + source = "IBM-Cloud/ibm" + version = "1.61.0" + } + } +} + +provider "ibm" { + region = "us-south" +} + +resource "ibm_database" "test_db1" { + name = "demo-postgres" + service = "databases-for-postgresql" + plan = "standard" + location = "eu-gb" + + group { + group_id = "member" + memory { + allocation_mb = 12288 + } + disk { + allocation_mb = 131072 + } + cpu { + allocation_count = 3 + } + } +} + +resource "ibm_database" "test_db2" { + name = "demo-postgres2" + service = "databases-for-postgresql" + plan = "standard" + location = "eu-gb" + + group { + group_id = "member" + memory { + allocation_mb = 15360 + } + members { + allocation_count = 4 + } + } +} diff --git a/internal/providers/terraform/ibm/testdata/database_test/database_test.usage.yml b/internal/providers/terraform/ibm/testdata/database_test/database_test.usage.yml new file mode 100644 index 00000000000..005e4927a0b --- /dev/null +++ b/internal/providers/terraform/ibm/testdata/database_test/database_test.usage.yml @@ -0,0 +1,9 @@ +version: 0.1 +resource_usage: + ibm_database.test_db1: + database_ram_mb: 12288 + database_disk_mb: 131072 + database_core: 3 + ibm_database.test_db2: + database_ram_mb: 15360 + database_members: 4 \ No newline at end of file diff --git a/internal/resources/ibm/database.go b/internal/resources/ibm/database.go new file mode 100644 index 00000000000..552f13ec092 --- /dev/null +++ b/internal/resources/ibm/database.go @@ -0,0 +1,190 @@ +package ibm + +import ( + "github.com/infracost/infracost/internal/resources" + "github.com/infracost/infracost/internal/schema" + "github.com/shopspring/decimal" + "github.com/tidwall/gjson" +) + +// Database struct represents a database instance +// +// This terraform resource is opaque and can handle multiple databases, provided with the right parameters +type Database struct { + Name string + Address string + Service string + Plan string + Location string + Group gjson.Result + + // Databases For PostgreSQL + // Catalog Link: https://cloud.ibm.com/catalog/services/databases-for-postgresql + // Pricing Link: https://cloud.ibm.com/docs/databases-for-postgresql?topic=databases-for-postgresql-pricing + RAM *int64 `infracost_usage:"database_ram_mb"` + Disk *int64 `infracost_usage:"database_disk_mb"` + Core *int64 `infracost_usage:"database_core"` + Members *int64 `infracost_usage:"database_members"` +} + +type DatabaseCostComponentsFunc func(*Database) []*schema.CostComponent + +// PopulateUsage parses the u schema.UsageData into the Database. +// It uses the `infracost_usage` struct tags to populate data into the Database. +func (r *Database) PopulateUsage(u *schema.UsageData) { + resources.PopulateArgsWithUsage(r, u) +} + +// DatabaseUsageSchema defines a list which represents the usage schema of Database. +var DatabaseUsageSchema = []*schema.UsageItem{ + {Key: "database_ram_mb", DefaultValue: 0, ValueType: schema.Int64}, + {Key: "database_disk_mb", DefaultValue: 0, ValueType: schema.Int64}, + {Key: "database_core", DefaultValue: 0, ValueType: schema.Int64}, + {Key: "database_members", DefaultValue: 0, ValueType: schema.Int64}, +} + +var DatabaseCostMap map[string]DatabaseCostComponentsFunc = map[string]DatabaseCostComponentsFunc{ + "databases-for-postgresql": GetPostgresCostComponents, + // "databases-for-etcd": + // "databases-for-redis": + // "databases-for-elasticsearch": + // "messages-for-rabbitmq": + // "databases-for-mongodb": + // "databases-for-mysql": + // "databases-for-cassandra": + // "databases-for-enterprisedb" +} + +func ConvertMBtoGB(d decimal.Decimal) decimal.Decimal { + return d.Div(decimal.NewFromInt(1024)) +} + +func PostgresRAMCostComponent(r *Database) *schema.CostComponent { + var R decimal.Decimal + if r.RAM != nil { + R = ConvertMBtoGB(decimal.NewFromInt(*r.RAM)) + } else { // set the default + R = decimal.NewFromInt(1) + } + var m decimal.Decimal + if r.Members != nil { + m = decimal.NewFromInt(*r.Members) + } else { // set the default + m = decimal.NewFromInt(2) + } + + cost := R.Mul(m) + + costComponent := schema.CostComponent{ + Name: "RAM", + Unit: "GB-RAM", + MonthlyQuantity: &cost, + UnitMultiplier: decimal.NewFromInt(1), + ProductFilter: &schema.ProductFilter{ + VendorName: strPtr("ibm"), + Region: strPtr(r.Location), + Service: strPtr("databases-for-postgresql"), + ProductFamily: strPtr("service"), + }, + PriceFilter: &schema.PriceFilter{ + Unit: strPtr("GIGABYTE_MONTHS_RAM"), + }, + } + return &costComponent +} + +func PostgresDiskCostComponent(r *Database) *schema.CostComponent { + var d decimal.Decimal + if r.Disk != nil { + d = ConvertMBtoGB(decimal.NewFromInt(*r.Disk)) + } else { // set the default + d = decimal.NewFromInt(5) + } + var m decimal.Decimal + if r.Members != nil { + m = decimal.NewFromInt(*r.Members) + } else { // set the default + m = decimal.NewFromInt(2) + } + + cost := d.Mul(m) + + costComponent := schema.CostComponent{ + Name: "Disk", + Unit: "GB-DISK", + MonthlyQuantity: &cost, + UnitMultiplier: decimal.NewFromInt(1), + ProductFilter: &schema.ProductFilter{ + VendorName: strPtr("ibm"), + Region: strPtr(r.Location), + Service: strPtr("databases-for-postgresql"), + ProductFamily: strPtr("service"), + }, + PriceFilter: &schema.PriceFilter{ + Unit: strPtr("GIGABYTE_MONTHS_DISK"), + }, + } + return &costComponent +} + +func PostgresCoreCostComponent(r *Database) *schema.CostComponent { + var c decimal.Decimal + if r.Core != nil { + c = decimal.NewFromInt(*r.Core) + } else { // set the default + c = decimal.NewFromInt(0) + } + var m decimal.Decimal + if r.Members != nil { + m = decimal.NewFromInt(*r.Members) + } else { // set the default + m = decimal.NewFromInt(2) + } + + cost := c.Mul(m) + + costComponent := schema.CostComponent{ + Name: "Core", + Unit: "Virtual Processor Core", + MonthlyQuantity: &cost, + UnitMultiplier: decimal.NewFromInt(1), + ProductFilter: &schema.ProductFilter{ + VendorName: strPtr("ibm"), + Region: strPtr(r.Location), + Service: strPtr("databases-for-postgresql"), + ProductFamily: strPtr("service"), + }, + PriceFilter: &schema.PriceFilter{ + Unit: strPtr("VIRTUAL_PROCESSOR_CORES"), + }, + } + return &costComponent +} + +func GetPostgresCostComponents(r *Database) []*schema.CostComponent { + return []*schema.CostComponent{ + PostgresRAMCostComponent(r), + PostgresDiskCostComponent(r), + PostgresCoreCostComponent(r), + } +} + +// BuildResource builds a schema.Resource from a valid Database struct. +// This method is called after the resource is initialised by an IaC provider. +// See providers folder for more information. +func (r *Database) BuildResource() *schema.Resource { + costComponentsFunc, ok := DatabaseCostMap[r.Service] + + if !ok { + return &schema.Resource{ + Name: r.Address, + UsageSchema: DatabaseUsageSchema, + } + } + + return &schema.Resource{ + Name: r.Address, + UsageSchema: DatabaseUsageSchema, + CostComponents: costComponentsFunc(r), + } +}