pricingapi.go 2.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  1. package otc
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "io"
  6. "net/http"
  7. "github.com/opencost/opencost/core/pkg/log"
  8. "github.com/opencost/opencost/pkg/cloud/httputil"
  9. )
  10. // http.DefaultClient has no timeout, so a hung pricing endpoint would block the
  11. // paginated fetch loop forever. Use the shared bounded client instead.
  12. var otcHTTPClient = httputil.BoundedClient()
  13. // Fetches and flattens all product entries across multiple services with pagination
  14. func (otc *OTC) fetchPaginatedProducts(serviceNames []string) ([]Product, error) {
  15. const baseURL = "https://calculator.otc-service.com/de/open-telekom-price-api/"
  16. var allProducts []Product
  17. limitFrom := 0
  18. query := buildServiceNameQueryParam(serviceNames)
  19. for {
  20. url := fmt.Sprintf("%s?%s&columns%%5B0%%5D=productIdParameter&columns%%5B1%%5D=opiFlavour&columns%%5B2%%5D=osUnit&columns%%5B3%%5D=vCpu&columns%%5B4%%5D=ram&columns%%5B5%%5D=priceAmount&limitFrom=%d", baseURL, query, limitFrom)
  21. resp, err := otcHTTPClient.Get(url)
  22. if err != nil {
  23. if resp != nil {
  24. resp.Body.Close()
  25. }
  26. log.Errorf("Error fetching products from OTC API: %v", err)
  27. return nil, err
  28. }
  29. if resp.StatusCode != http.StatusOK {
  30. io.Copy(io.Discard, resp.Body)
  31. resp.Body.Close()
  32. return nil, fmt.Errorf("OTC API returned unexpected status %d", resp.StatusCode)
  33. }
  34. pageData, stats, err := otc.loadPaginatedResponse(resp)
  35. resp.Body.Close()
  36. if err != nil {
  37. log.Errorf("Error loading paginated response: %v", err)
  38. return nil, err
  39. }
  40. for _, products := range pageData {
  41. allProducts = append(allProducts, products...)
  42. }
  43. if stats.CurrentPage >= stats.MaxPages {
  44. log.Infof("Fetched all products for services: %v", serviceNames)
  45. break
  46. }
  47. limitFrom += stats.RecordsPerPage
  48. }
  49. return allProducts, nil
  50. }
  51. // Parses the OTC API response into a map of service → []Product and pagination stats
  52. func (otc *OTC) loadPaginatedResponse(resp *http.Response) (map[string][]Product, *OTCStats, error) {
  53. body, err := io.ReadAll(resp.Body)
  54. if err != nil {
  55. log.Errorf("Error reading OTC API response: %v", err)
  56. return nil, nil, err
  57. }
  58. var raw map[string]map[string]json.RawMessage
  59. if err := json.Unmarshal(body, &raw); err != nil {
  60. log.Errorf("Error unmarshalling OTC API response: %v", err)
  61. return nil, nil, err
  62. }
  63. var data map[string][]Product
  64. if err := json.Unmarshal(raw["response"]["result"], &data); err != nil {
  65. log.Errorf("Error unmarshalling result section: %v", err)
  66. return nil, nil, err
  67. }
  68. var stats OTCStats
  69. if err := json.Unmarshal(raw["response"]["stats"], &stats); err != nil {
  70. log.Errorf("Error unmarshalling stats section: %v", err)
  71. return nil, nil, err
  72. }
  73. return data, &stats, nil
  74. }