cluster_test.go 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. package test
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. // "math"
  7. // "net"
  8. "net/http"
  9. "strconv"
  10. // "testing"
  11. // "time"
  12. // "gotest.tools/assert"
  13. prometheusClient "github.com/prometheus/client_golang/api"
  14. // v1 "k8s.io/api/core/v1"
  15. // metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  16. _ "k8s.io/client-go/plugin/pkg/client/auth"
  17. "log"
  18. )
  19. const apiPrefix = "/api/v1"
  20. const epQuery = apiPrefix + "/query"
  21. // The integration test assumes a GKE cluster in us-central-1 or an AWS cluster in us-east-2, with the following instance types
  22. // and storage classes.
  23. var prices = map[string]float64{
  24. "n1standardRAM": 0.004237,
  25. "n1standardCPU": 0.031611,
  26. "t2.medium": 0.0464,
  27. "t2.small": 0.023,
  28. "t2.micro": 0.0116,
  29. "c4.large": 0.1,
  30. "gp2": 0.000137,
  31. "ssd": 0.170,
  32. "Standard_DS2_v2": 0.252,
  33. "g1smallCPU": 0.025643,
  34. "g1smallRAM": 0.000034,
  35. "n1-highmem-2": 0.1171,
  36. }
  37. func parseQuery(qr interface{}) (float64, error) {
  38. data, ok := qr.(map[string]interface{})["data"]
  39. if !ok {
  40. return 0, fmt.Errorf("Improperly formatted response from prometheus, response %+v has no data field", data)
  41. }
  42. r, ok := data.(map[string]interface{})["result"]
  43. if !ok {
  44. return 0, fmt.Errorf("Improperly formatted data from prometheus, data has no result field")
  45. }
  46. results, ok := r.([]interface{})
  47. if !ok {
  48. return 0, fmt.Errorf("Improperly formatted results from prometheus, result field is not a slice")
  49. }
  50. val, ok := results[0].(map[string]interface{})["value"]
  51. if !ok {
  52. return 0, fmt.Errorf("Improperly formatted results from prometheus, value is not a field in the vector")
  53. }
  54. dataPoint, ok := val.([]interface{})
  55. if !ok || len(dataPoint) != 2 {
  56. return 0, fmt.Errorf("Improperly formatted datapoint from Prometheus")
  57. }
  58. return strconv.ParseFloat(dataPoint[1].(string), 64)
  59. }
  60. func query(cli prometheusClient.Client, query string) (interface{}, error) {
  61. u := cli.URL(epQuery, nil)
  62. q := u.Query()
  63. q.Set("query", query)
  64. u.RawQuery = q.Encode()
  65. req, err := http.NewRequest(http.MethodGet, u.String(), nil)
  66. if err != nil {
  67. return nil, err
  68. }
  69. _, body, _, err := cli.Do(context.Background(), req)
  70. if err != nil {
  71. return nil, err
  72. }
  73. var toReturn interface{}
  74. err = json.Unmarshal(body, &toReturn)
  75. if err != nil {
  76. log.Printf("ERROR" + err.Error())
  77. }
  78. return toReturn, err
  79. }
  80. /*
  81. func TestKubernetesPVCosts(t *testing.T) {
  82. cli, err := getKubernetesClient()
  83. if err != nil {
  84. panic(err)
  85. }
  86. var LongTimeoutRoundTripper http.RoundTripper = &http.Transport{ // may be necessary for long prometheus queries. TODO: make this configurable
  87. Proxy: http.ProxyFromEnvironment,
  88. DialContext: (&net.Dialer{
  89. Timeout: 120 * time.Second,
  90. KeepAlive: 120 * time.Second,
  91. }).DialContext,
  92. TLSHandshakeTimeout: 10 * time.Second,
  93. }
  94. pc := prometheusClient.Config{
  95. Address: address,
  96. RoundTripper: LongTimeoutRoundTripper,
  97. }
  98. promCli, err := prometheusClient.NewClient(pc)
  99. if err != nil {
  100. panic(err)
  101. }
  102. pvs, err := cli.CoreV1().PersistentVolumes().List(metav1.ListOptions{})
  103. if err != nil {
  104. panic(err)
  105. }
  106. for _, pv := range pvs.Items {
  107. name := pv.Name
  108. class := pv.Spec.StorageClassName
  109. q := fmt.Sprintf(`pv_hourly_cost{persistentvolume="%s"}`, name)
  110. qt, err := query(promCli, q)
  111. total, err := parseQuery(qt)
  112. if err != nil {
  113. log.Printf(err.Error())
  114. }
  115. if price, ok := prices[class]; ok {
  116. assert.Equal(t, math.Round(total*1000000)/1000000, price)
  117. }
  118. }
  119. }
  120. func TestKubernetesClusterCosts(t *testing.T) {
  121. prices["n1-standard-1"] = math.Round((prices["n1standardCPU"]+3.61219*prices["n1standardRAM"])*10000) / 10000
  122. prices["g1-small"] = math.Round(((prices["g1smallCPU"] + 0.216998*prices["g1smallRAM"]) * 10000)) / 10000
  123. cli, err := getKubernetesClient()
  124. if err != nil {
  125. panic(err)
  126. }
  127. var LongTimeoutRoundTripper http.RoundTripper = &http.Transport{ // may be necessary for long prometheus queries. TODO: make this configurable
  128. Proxy: http.ProxyFromEnvironment,
  129. DialContext: (&net.Dialer{
  130. Timeout: 120 * time.Second,
  131. KeepAlive: 120 * time.Second,
  132. }).DialContext,
  133. TLSHandshakeTimeout: 10 * time.Second,
  134. }
  135. pc := prometheusClient.Config{
  136. Address: address,
  137. RoundTripper: LongTimeoutRoundTripper,
  138. }
  139. promCli, err := prometheusClient.NewClient(pc)
  140. if err != nil {
  141. panic(err)
  142. }
  143. nodes, err := cli.CoreV1().Nodes().List(metav1.ListOptions{})
  144. if err != nil {
  145. panic(err)
  146. }
  147. for _, n := range nodes.Items {
  148. name := n.GetObjectMeta().GetName()
  149. q := fmt.Sprintf(`node_total_hourly_cost{instance="%s"}`, name)
  150. labels := n.GetObjectMeta().GetLabels()
  151. instanceType := labels[v1.LabelInstanceType]
  152. qt, err := query(promCli, q)
  153. if err != nil {
  154. panic(err)
  155. }
  156. total, err := parseQuery(qt)
  157. if err != nil {
  158. panic(err)
  159. }
  160. if price, ok := prices[instanceType]; ok {
  161. assert.Equal(t, math.Round(total*10000)/10000, price)
  162. }
  163. }
  164. }
  165. */