-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathclient.go
326 lines (280 loc) · 8.84 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
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
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
/*
* Copyright 2020 Armory, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License")
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package plank
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"math"
"net"
"net/http"
"runtime"
"time"
)
const (
// SpinFiatUserHeader is the header name used for representing users.
SpinFiatUserHeader string = "X-Spinnaker-User"
// SpinFiatAccountHeader is the header name used for representing accounts.
SpinFiatAccountHeader string = "X-Spinnaker-Accounts"
ApplicationJson ContentType = "application/json"
ApplicationContextJson ContentType = "application/context+json"
)
type ErrUnsupportedStatusCode struct {
Code int
}
func (e *ErrUnsupportedStatusCode) Error() string {
return fmt.Sprintf("unsupported status code: %d", e.Code)
}
// Client for working with API servers that accept and return JSON payloads.
type Client struct {
http *http.Client
retryIncrement time.Duration
maxRetry int
URLs map[string]string
FiatUser string
ArmoryEndpoints bool
UseGate bool
}
type ContentType string
type ClientOption func(*Client)
func WithClient(client *http.Client) ClientOption {
return func(c *Client) {
c.http = client
}
}
func WithTransport(transport *http.Transport) ClientOption {
return func(c *Client) {
c.http.Transport = transport
}
}
func WithRetryIncrement(t time.Duration) ClientOption {
return func(c *Client) {
c.retryIncrement = t
}
}
func WithMaxRetries(retries int) ClientOption {
return func(c *Client) {
c.maxRetry = retries
}
}
func WithFiatUser(user string) ClientOption {
return func(c *Client) {
c.FiatUser = user
}
}
func WithOverrideAllURLs(urls map[string]string) ClientOption {
return func(c *Client) {
c.URLs = make(map[string]string)
for k, v := range urls {
c.URLs[k] = v
}
}
}
// DefaultURLs
var DefaultURLs = map[string]string{
"orca": "http://localhost:8083",
"front50": "http://localhost:8080",
"fiat": "http://localhost:7003",
"gate": "http://localhost:8084",
}
// New constructs a Client using a default client and sane non-shared http transport
func New(opts ...ClientOption) *Client {
httpClient := &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).DialContext,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
MaxIdleConnsPerHost: runtime.GOMAXPROCS(0) + 1,
},
}
c := &Client{
http: httpClient,
retryIncrement: 100,
maxRetry: 20,
URLs: make(map[string]string),
UseGate: false,
}
// Have to manually copy the DefaultURLs map because otherwise any changes
// made to this copy will modify the global. I can't believe I have to
// to do this in this day and age...
for k, v := range DefaultURLs {
c.URLs[k] = v
}
for _, opt := range opts {
opt(c)
}
return c
}
// FailedResponse captures a 4xx/5xx response from the upstream Spinnaker service.
// It is expected that the caller destructures the response according to the structure they expect.
type FailedResponse struct {
Response []byte
StatusCode int
}
func (e *FailedResponse) Error() string {
return fmt.Sprintf("%v: %s", http.StatusText(e.StatusCode), string(e.Response))
}
// Method represents a supported HTTP Method in Plank.
type Method string
const (
// Patch is a PATCH HTTP method
Patch Method = http.MethodPatch
// Post is a POST HTTP method
Post Method = http.MethodPost
// Put is a PUT HTTP method
Put Method = http.MethodPut
// Get is a GET HTTP method
Get Method = http.MethodGet
)
type RequestFunction func() error
func (c *Client) RequestWithRetry(f RequestFunction) error {
var err error
for i := 0; i <= c.maxRetry; i++ {
err = f()
if err == nil {
return nil // Success (or non-failure)
}
// exponential back-off
interval := c.retryIncrement * time.Duration(math.Exp2(float64(i)))
time.Sleep(interval)
}
// We get here, we timed out without getting a valid response.
return err
}
func (c *Client) request(method Method, url string, contentType ContentType, body interface{}, dest interface{}, traceparent string) error {
jsonBody, err := json.Marshal(body)
if err != nil {
return fmt.Errorf("could not %q, body could not be marshalled to json: %w", string(method), err)
}
req, err := http.NewRequest(string(method), url, bytes.NewBuffer(jsonBody))
if err != nil {
return fmt.Errorf("could not %q, new request could not be made: %w", string(method), err)
}
if c.FiatUser != "" {
req.Header.Set(SpinFiatUserHeader, c.FiatUser)
req.Header.Set(SpinFiatAccountHeader, c.FiatUser)
}
if traceparent != "" {
req.Header.Set("traceparent", traceparent)
}
req.Header.Set("Content-Type", string(contentType))
resp, err := c.http.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
if string(b) == "" {
dest = make(map[string]interface{})
return nil
}
return json.Unmarshal(b, dest)
} else if resp.StatusCode >= 400 && resp.StatusCode < 600 {
return &FailedResponse{StatusCode: resp.StatusCode, Response: b}
} else {
// If status falls outside the range of 200 - 599 then return an error.
return &ErrUnsupportedStatusCode{Code: resp.StatusCode}
}
}
// Get a JSON payload from the URL then decode it into the 'dest' arguement.
func (c *Client) Get(url, traceparent string, dest interface{}) error {
return c.request(Get, url, ApplicationJson, nil, dest, traceparent)
}
func (c *Client) GetWithRetry(url, traceparent string, dest interface{}) error {
return c.RequestWithRetry(func() error {
return c.Get(url, traceparent, dest)
})
}
// Patch updates a resource for the target URL
func (c *Client) Patch(url, traceparent string, contentType ContentType, body interface{}, dest interface{}) error {
return c.request(Patch, url, contentType, body, dest, traceparent)
}
// PatchWithRetry updates a resource for the target URL
func (c *Client) PatchWithRetry(url, traceparent string, contentType ContentType, body interface{}, dest interface{}) error {
return c.RequestWithRetry(func() error {
return c.Patch(url, traceparent, contentType, body, dest)
})
}
// Post a JSON payload from the URL then decode it into the 'dest' arguement.
func (c *Client) Post(url, traceparent string, contentType ContentType, body interface{}, dest interface{}) error {
return c.request(Post, url, contentType, body, dest, traceparent)
}
func (c *Client) PostWithRetry(url, traceparent string, contentType ContentType, body interface{}, dest interface{}) error {
return c.RequestWithRetry(func() error {
return c.Post(url, traceparent, contentType, body, dest)
})
}
// Post a JSON payload from the URL then decode it into the 'dest' arguement.
func (c *Client) Put(url, traceparent string, contentType ContentType, body interface{}, dest interface{}) error {
return c.request(Put, url, contentType, body, dest, traceparent)
}
func (c *Client) PutWithRetry(url, traceparent string, contentType ContentType, body interface{}, dest interface{}) error {
return c.RequestWithRetry(func() error {
return c.Put(url, traceparent, contentType, body, dest)
})
}
func (c *Client) Delete(url, traceparent string) error {
request, err := http.NewRequest("DELETE", url, nil)
if err != nil {
return err
}
if "" != c.FiatUser {
request.Header.Set(SpinFiatUserHeader, c.FiatUser)
}
if traceparent != "" {
request.Header.Set("traceparent", traceparent)
}
resp, err := c.http.Do(request)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode >= 200 && resp.StatusCode < 400 {
// There is no support for receiving a payload back from a DELETE...
return nil
}
return &ErrUnsupportedStatusCode{Code: resp.StatusCode}
}
func (c *Client) DeleteWithRetry(url, traceparent string) error {
return c.RequestWithRetry(func() error {
return c.Delete(url, traceparent)
})
}
func (c *Client) ArmoryEndpointsEnabled() bool {
return c.ArmoryEndpoints
}
func (c *Client) EnableArmoryEndpoints() {
c.ArmoryEndpoints = true
}
func (c *Client) DisableARmoryEndpoints() {
c.ArmoryEndpoints = false
}
func (c *Client) UseGateEndpoints() {
c.UseGate = true
}
func (c *Client) UseServiceEndpoints() {
c.UseGate = false
}
//Yes a blank sorta line has to be here. Bug in golang