/
client.go
150 lines (119 loc) 路 3.98 KB
/
client.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
package apiclient
import (
"bytes"
"fmt"
"io"
"net/http"
"strings"
json "github.com/json-iterator/go"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/tidwall/gjson"
"github.com/infracost/infracost/internal/logging"
"github.com/infracost/infracost/internal/version"
)
type APIClient struct {
httpClient *http.Client
endpoint string
apiKey string
uuid uuid.UUID
}
type GraphQLQuery struct {
Query string `json:"query"`
Variables map[string]interface{} `json:"variables"`
}
var (
ErrorCodeExceededQuota = "above_quota"
ErrorCodeAPIKeyInvalid = "invalid_api_key"
)
// APIError defines an api error that is designed to be showed to the user in a
// output form (comment/stdout/html).
type APIError struct {
err error
// Msg defines a human-readable string that is safe to show to the user
// to give more context about an error.
Msg string
// Code is the original StatusCode of the error.
Code int
// ErrorCode is the internal error code that can accompany errors from different status codes.
ErrorCode string
}
func (e *APIError) Error() string {
return fmt.Sprintf("%s: %v", e.Msg, e.err.Error())
}
type APIErrorResponse struct {
Error string `json:"error"`
ErrorCode string `json:"error_code"`
}
func (c *APIClient) DoQueries(queries []GraphQLQuery) ([]gjson.Result, error) {
if len(queries) == 0 {
log.Debug().Msg("Skipping GraphQL request as no queries have been specified")
return []gjson.Result{}, nil
}
respBody, err := c.doRequest("POST", "/graphql", queries)
return gjson.ParseBytes(respBody).Array(), err
}
func (c *APIClient) doRequest(method string, path string, d interface{}) ([]byte, error) {
logging.Logger.Debug().Msgf("'%s' request to '%s' using trace_id: '%s'", method, path, c.uuid.String())
reqBody, err := json.Marshal(d)
if err != nil {
return []byte{}, errors.Wrap(err, "Error generating request body")
}
req, err := http.NewRequest(method, c.endpoint+path, bytes.NewBuffer(reqBody))
if err != nil {
return []byte{}, errors.Wrap(err, "Error generating request")
}
c.AddAuthHeaders(req)
client := c.httpClient
if client == nil {
client = http.DefaultClient
}
resp, err := client.Do(req)
if err != nil {
return []byte{}, errors.Wrap(err, "Error sending API request")
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return []byte{}, &APIError{err: err, Msg: fmt.Sprintf("Invalid API response %s %s", method, path)}
}
if resp.StatusCode != 200 {
var r APIErrorResponse
err = json.Unmarshal(respBody, &r)
if err != nil {
return []byte{}, &APIError{err: err, Msg: fmt.Sprintf("Invalid API response %q %q body: %q", method, path, respBody), Code: resp.StatusCode}
}
if r.ErrorCode != "" {
return []byte{}, &APIError{err: fmt.Errorf("%v %v", resp.Status, r.Error), Msg: r.Error, Code: resp.StatusCode, ErrorCode: r.ErrorCode}
}
// handle legacy cloud pricing apis which don't have the new `error_code` field.
if r.Error == "Invalid API key" {
return []byte{}, &APIError{err: fmt.Errorf("%v %v", resp.Status, r.Error), Msg: "Invalid API Key", Code: resp.StatusCode, ErrorCode: ErrorCodeAPIKeyInvalid}
}
return []byte{}, &APIError{err: fmt.Errorf("%v %v", resp.Status, r.Error), Msg: "Received error from API", Code: resp.StatusCode}
}
return respBody, nil
}
func (c *APIClient) AddDefaultHeaders(req *http.Request) {
req.Header.Set("content-type", "application/json")
req.Header.Set("User-Agent", userAgent())
}
func (c *APIClient) AddAuthHeaders(req *http.Request) {
c.AddDefaultHeaders(req)
if strings.HasPrefix(c.apiKey, "ics") {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.apiKey))
} else {
req.Header.Set("X-Api-Key", c.apiKey)
}
if c.uuid != uuid.Nil {
req.Header.Set("X-Infracost-Trace-Id", fmt.Sprintf("cli=%s", c.uuid.String()))
}
}
func userAgent() string {
userAgent := "infracost"
if version.Version != "" {
userAgent += fmt.Sprintf("-%s", version.Version)
}
return userAgent
}