cluster_test.go 5.4 KB

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