/
collins.go
290 lines (248 loc) · 7.54 KB
/
collins.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
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
package collins
import (
"bufio"
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
"path"
"strconv"
"strings"
yaml "gopkg.in/yaml.v2"
)
const (
maxHTTPCode = 299
)
var (
VERSION = "0.1.0"
)
// Client represents a client connection to a collins server. Requests to the
// various APIs are done by calling functions on the various services.
type Client struct {
client *http.Client
BaseURL *url.URL
User string
Password string
Assets *AssetService
AssetTypes *AssetTypeService
Logs *LogService
States *StateService
Tags *TagService
Management *ManagementService
IPAM *IPAMService
Firehose *FirehoseService
}
// Error represents an error returned from collins. Collins returns
// errors in JSON format, which we marshal in to this struct.
type Error struct {
Status string `json:"status"`
Data struct {
Message string `json:"message"`
} `json:"data"`
}
// Container is used to deserialize the JSON reponse from the API.
type Container struct {
CollinsStatus string `json:"status"`
Data interface{} `json:"data"`
}
// Response is our custom response type. It has the HTTP response embedded for
// debugging purposes. It also has embedded the `container` that the JSON
// response gets decoded into (if the caller to `Do` passes in a struct
// to decode into). Finally it contains all necessary data for pagination.
type Response struct {
*http.Response
*Container
PreviousPage int
CurrentPage int
NextPage int
TotalResults int
}
// PageOpts allows the caller to specify pagination options. Since Collins takes
// in pagination options via URL parameters we can use google/go-querystring to
// describe our pagination opts as structs. This also allows embedding of
// pagination options directly into other request option structs.
type PageOpts struct {
Page int `url:"page,omitempty"`
Size int `url:"size,omitempty"`
Sort string `url:"sort,omitempty"`
SortField string `url:"sortField,omitempty"`
}
// PaginationResponse is used to represent the pagination information coming
// back from the collins server.
type PaginationResponse struct {
PreviousPage int `json:"PreviousPage"`
CurrentPage int `json:"CurrentPage"`
NextPage int `json:"NextPage"`
TotalResults int `json:"TotalResults"`
}
func (e *Error) Error() string {
return e.Data.Message
}
// NewClient creates a Client struct and returns a point to it. This client is
// then used to query the various APIs collins provides.
func NewClient(username, password, baseurl string) (*Client, error) {
u, err := url.Parse(baseurl)
if err != nil {
return nil, err
}
c := &Client{
client: &http.Client{},
User: username,
Password: password,
BaseURL: u,
}
c.Assets = &AssetService{client: c}
c.AssetTypes = &AssetTypeService{client: c}
c.Logs = &LogService{client: c}
c.States = &StateService{client: c}
c.Tags = &TagService{client: c}
c.Management = &ManagementService{client: c}
c.IPAM = &IPAMService{client: c}
c.Firehose = &FirehoseService{client: c}
return c, nil
}
// NewClientFromYaml sets up a new Client, but reads the credentials and host
// from a yaml file on disk. The following paths are searched:
//
// * Path in COLLINS_CLIENT_CONFIG environment variable
// * ~/.collins.yml
// * /etc/collins.yml
// * /var/db/collins.yml
func NewClientFromYaml() (*Client, error) {
yamlPaths := []string{
os.Getenv("COLLINS_CLIENT_CONFIG"),
path.Join(os.Getenv("HOME"), ".collins.yml"),
"/etc/collins.yml",
"/var/db/collins.yml",
}
return NewClientFromFiles(yamlPaths...)
}
// NewClientFromFiles takes an array of paths to look for credentials, and
// returns a Client based on the first config file that exists and parses
// correctly. Otherwise, it returns nil and an error.
func NewClientFromFiles(paths ...string) (*Client, error) {
f, err := openYamlFiles(paths...)
if err != nil {
return nil, err
}
data, err := ioutil.ReadAll(f)
if err != nil {
return nil, err
}
var creds struct {
Host string
Username string
Password string
}
err = yaml.Unmarshal(data, &creds)
if err != nil {
return nil, err
}
return NewClient(creds.Username, creds.Password, creds.Host)
}
func openYamlFiles(paths ...string) (io.Reader, error) {
for _, path := range paths {
f, err := os.Open(path)
if err != nil {
continue
} else {
return f, nil
}
}
errStr := fmt.Sprintf("Could not load collins credentials from file. (Searched: %s)", strings.Join(paths, ", "))
return nil, errors.New(errStr)
}
// NewRequest creates a new HTTP request which can then be performed by Do.
func (c *Client) NewRequest(method, path string) (*http.Request, error) {
rel, err := url.Parse(path)
if err != nil {
return nil, err
}
reqURL := c.BaseURL.ResolveReference(rel)
req, err := http.NewRequest(method, reqURL.String(), nil)
if err != nil {
return nil, err
}
req.SetBasicAuth(c.User, c.Password)
req.Header.Set("User-Agent", "go-collins "+VERSION)
req.Header.Set("Accept", "application/json")
return req, nil
}
// Create our custom response object that we will pass back to caller
func newResponse(r *http.Response) *Response {
resp := &Response{Response: r}
resp.populatePagination()
return resp
}
// Read in data from headers and use that to populate our response struct
func (r *Response) populatePagination() {
h := r.Header
if prev := h.Get("X-Pagination-PreviousPage"); prev != "" {
n, _ := strconv.Atoi(prev)
r.PreviousPage = n
}
if cur := h.Get("X-Pagination-CurrentPage"); cur != "" {
n, _ := strconv.Atoi(cur)
r.CurrentPage = n
}
if next := h.Get("X-Pagination-NextPage"); next != "" {
n, _ := strconv.Atoi(next)
r.NextPage = n
}
if total := h.Get("X-Pagination-TotalResults"); total != "" {
n, _ := strconv.Atoi(total)
r.TotalResults = n
}
}
// Do performs a given request that was built with `NewRequest`. We return the
// response object as well so that callers can have access to pagination info.
func (c *Client) Do(req *http.Request, v interface{}) (*Response, error) {
resp, err := c.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
response := newResponse(resp)
if resp.StatusCode > maxHTTPCode {
collinsError := new(Error)
if strings.Contains(resp.Header.Get("Content-Type"), "application/json;") {
err = json.NewDecoder(resp.Body).Decode(collinsError)
if err != nil {
return response, err
}
} else if strings.Contains(resp.Header.Get("Content-Type"), "text/plain;") {
errbuf := &bytes.Buffer{}
bufio.NewReader(resp.Body).WriteTo(errbuf)
collinsError.Data.Message = errbuf.String()
} else {
errstr := fmt.Sprintf("Response with unexpected Content-Type - `%s' received.", resp.Header.Get("Content-Type"))
return response, errors.New(errstr)
}
collinsError.Data.Message = resp.Status + " returned from collins: " + collinsError.Data.Message
return response, collinsError
}
// This looks kind of weird but it works. This allows callers that pass in
// an interface to have response JSON decoded into the interface they pass.
// It also allows accessing `response.container.Status` etc. to get helpful
// response info from Collins.
if v != nil {
response.Container = &Container{
Data: v,
}
}
if strings.Contains(resp.Header.Get("Content-Type"), "application/json;") {
err = json.NewDecoder(resp.Body).Decode(response)
if err != nil {
return response, err
}
} else {
errstr := fmt.Sprintf("Response with unexpected Content-Type - `%s' received. Erroring out.", resp.Header.Get("Content-Type"))
return response, errors.New(errstr)
}
return response, nil
}