| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228 |
- package stackit
- import (
- "context"
- "fmt"
- "strings"
- "time"
- "github.com/opencost/opencost/core/pkg/log"
- "github.com/opencost/opencost/core/pkg/opencost"
- "github.com/opencost/opencost/pkg/cloud"
- "github.com/stackitcloud/stackit-sdk-go/core/config"
- cost "github.com/stackitcloud/stackit-sdk-go/services/cost/v3api"
- )
- type CostIntegration struct {
- CostConfiguration
- ConnectionStatus cloud.ConnectionStatus
- }
- func (ci *CostIntegration) GetCloudCost(start time.Time, end time.Time) (*opencost.CloudCostSetRange, error) {
- var opts []config.ConfigurationOption
- if ci.ServiceAccountKeyPath != "" {
- opts = append(opts, config.WithServiceAccountKeyPath(ci.ServiceAccountKeyPath))
- }
- client, err := cost.NewAPIClient(opts...)
- if err != nil {
- ci.ConnectionStatus = cloud.FailedConnection
- return nil, fmt.Errorf("creating STACKIT cost API client: %w", err)
- }
- fromStr := start.Format("2006-01-02")
- // STACKIT Cost API uses inclusive end dates; OpenCost windows are end-exclusive,
- // so subtract one day to align.
- toStr := end.AddDate(0, 0, -1).Format("2006-01-02")
- resp, err := client.DefaultAPI.
- GetCostsForProject(context.Background(), ci.CustomerAccountID, ci.ProjectID).
- From(fromStr).
- To(toStr).
- Depth("service").
- Granularity("daily").
- Execute()
- if err != nil {
- ci.ConnectionStatus = cloud.FailedConnection
- return nil, fmt.Errorf("querying STACKIT costs: %w", err)
- }
- ccsr, err := opencost.NewCloudCostSetRange(start, end, opencost.AccumulateOptionDay, ci.Key())
- if err != nil {
- return nil, err
- }
- if resp == nil || resp.ProjectCostWithDetailedServices == nil {
- if ci.ConnectionStatus != cloud.SuccessfulConnection {
- ci.ConnectionStatus = cloud.MissingData
- }
- return ccsr, nil
- }
- detailed := resp.ProjectCostWithDetailedServices
- for _, svc := range detailed.GetServices() {
- serviceName := svc.GetServiceName()
- category := selectSTACKITCategory(serviceName)
- sku := svc.GetSku()
- regionID := extractRegionFromServiceName(serviceName)
- reportData := svc.GetReportData()
- if len(reportData) == 0 {
- // No daily granularity data; use total charge
- totalChargeCents := svc.GetTotalCharge()
- totalDiscountCents := svc.GetTotalDiscount()
- totalCharge := totalChargeCents / 100.0
- totalDiscount := totalDiscountCents / 100.0
- netCost := totalCharge
- properties := &opencost.CloudCostProperties{
- Provider: opencost.STACKITProvider,
- AccountID: ci.CustomerAccountID,
- InvoiceEntityID: ci.CustomerAccountID,
- RegionID: regionID,
- Service: serviceName,
- Category: category,
- ProviderID: sku,
- Labels: opencost.CloudCostLabels{},
- }
- listCost := totalCharge + totalDiscount
- cc := &opencost.CloudCost{
- Properties: properties,
- Window: opencost.NewWindow(&start, &end),
- ListCost: opencost.CostMetric{
- Cost: listCost,
- },
- NetCost: opencost.CostMetric{
- Cost: netCost,
- },
- AmortizedNetCost: opencost.CostMetric{
- Cost: netCost,
- },
- AmortizedCost: opencost.CostMetric{
- Cost: listCost,
- },
- InvoicedCost: opencost.CostMetric{
- Cost: netCost,
- },
- }
- ccsr.LoadCloudCost(cc)
- continue
- }
- for _, rd := range reportData {
- chargeCents := rd.GetCharge()
- discountCents := rd.GetDiscount()
- charge := chargeCents / 100.0
- discount := discountCents / 100.0
- tp := rd.GetTimePeriod()
- periodStart, periodEnd := parsePeriod(tp.GetStart(), tp.GetEnd(), start, end)
- properties := &opencost.CloudCostProperties{
- Provider: opencost.STACKITProvider,
- AccountID: ci.CustomerAccountID,
- InvoiceEntityID: ci.CustomerAccountID,
- RegionID: regionID,
- Service: serviceName,
- Category: category,
- ProviderID: sku,
- Labels: opencost.CloudCostLabels{},
- }
- listCost := charge + discount
- cc := &opencost.CloudCost{
- Properties: properties,
- Window: opencost.NewWindow(&periodStart, &periodEnd),
- ListCost: opencost.CostMetric{
- Cost: listCost,
- },
- NetCost: opencost.CostMetric{
- Cost: charge,
- },
- AmortizedNetCost: opencost.CostMetric{
- Cost: charge,
- },
- AmortizedCost: opencost.CostMetric{
- Cost: listCost,
- },
- InvoicedCost: opencost.CostMetric{
- Cost: charge,
- },
- }
- ccsr.LoadCloudCost(cc)
- }
- }
- ci.ConnectionStatus = cloud.SuccessfulConnection
- return ccsr, nil
- }
- // parsePeriod parses start/end date strings from the STACKIT API, falling back to the given defaults.
- func parsePeriod(startStr, endStr string, defaultStart, defaultEnd time.Time) (time.Time, time.Time) {
- periodStart := defaultStart
- periodEnd := defaultEnd
- if startStr != "" {
- if t, err := time.Parse("2006-01-02", startStr); err == nil {
- periodStart = t
- } else if t, err := time.Parse(time.RFC3339, startStr); err == nil {
- periodStart = t
- }
- }
- if endStr != "" {
- if t, err := time.Parse("2006-01-02", endStr); err == nil {
- // End date is inclusive in the API, add one day for the window
- periodEnd = t.AddDate(0, 0, 1)
- } else if t, err := time.Parse(time.RFC3339, endStr); err == nil {
- periodEnd = t
- }
- }
- return periodStart, periodEnd
- }
- func (ci *CostIntegration) GetStatus() cloud.ConnectionStatus {
- if ci.ConnectionStatus.String() == "" {
- ci.ConnectionStatus = cloud.InitialStatus
- }
- return ci.ConnectionStatus
- }
- func (ci *CostIntegration) RefreshStatus() cloud.ConnectionStatus {
- log.Warn("status refresh is not supported for the STACKIT provider")
- return ci.ConnectionStatus
- }
- // extractRegionFromServiceName extracts the region suffix from a STACKIT Cost API
- // service name (e.g. "Tiny Server-t1.2-EU01" -> "eu01").
- func extractRegionFromServiceName(serviceName string) string {
- idx := strings.LastIndex(serviceName, "-")
- if idx >= 0 {
- suffix := strings.ToLower(serviceName[idx+1:])
- if strings.HasPrefix(suffix, "eu") || strings.HasPrefix(suffix, "us") {
- return suffix
- }
- }
- return "eu01"
- }
- func selectSTACKITCategory(serviceName string) string {
- lower := strings.ToLower(serviceName)
- switch {
- case strings.Contains(lower, "compute") || strings.Contains(lower, "server") || strings.Contains(lower, "ske"):
- return opencost.ComputeCategory
- case strings.Contains(lower, "storage") || strings.Contains(lower, "object store") || strings.Contains(lower, "backup"):
- return opencost.StorageCategory
- case strings.Contains(lower, "network") || strings.Contains(lower, "load balancer") || strings.Contains(lower, "dns"):
- return opencost.NetworkCategory
- default:
- return opencost.OtherCategory
- }
- }
|