-
Notifications
You must be signed in to change notification settings - Fork 8
/
books.go
375 lines (309 loc) · 12.1 KB
/
books.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
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
// Package common implements shared functions and structs between various book* applications
package common
import (
"encoding/json"
"fmt"
"math/rand"
"net/http"
"os"
"strconv"
"strings"
"sync/atomic"
"time"
"github.com/flomesh-io/fsm/pkg/logger"
"github.com/flomesh-io/fsm/pkg/utils"
)
// BookBuyerPurchases is all of the books that the bookbuyer has bought
type BookBuyerPurchases struct {
BooksBought int64 `json:"booksBought"`
BooksBoughtV1 int64 `json:"booksBoughtV1"`
BooksBoughtV2 int64 `json:"booksBoughtV2"`
}
// BookThiefThievery is all of the books the bookthief has stolen
type BookThiefThievery struct {
BooksStolen int64 `json:"booksStolen"`
BooksStolenV1 int64 `json:"booksStolenV1"`
BooksStolenV2 int64 `json:"booksStolenV2"`
}
// BookStorePurchases are all of the books sold from the bookstore
type BookStorePurchases struct {
BooksSold int64 `json:"booksSold"`
}
const (
// RestockWarehouseURL is a header string constant.
RestockWarehouseURL = "restock-books"
// bookstorePort is the bookstore service's port
bookstorePort = 14001
// bookwarehousePort is the bookwarehouse service's port
bookwarehousePort = 14001
httpPrefix = "http://"
httpsPrefix = "https://"
)
var (
// enableEgress determines whether egress is enabled
enableEgress = os.Getenv(EnableEgressEnvVar) == "true"
sleepDurationBetweenRequestsSecondsStr = utils.GetEnv("CI_SLEEP_BETWEEN_REQUESTS_SECONDS", "1")
minSuccessThresholdStr = utils.GetEnv("CI_MIN_SUCCESS_THRESHOLD", "1")
maxIterationsStr = utils.GetEnv("CI_MAX_ITERATIONS_THRESHOLD", "0") // 0 for unlimited
bookstoreServiceName = utils.GetEnv("BOOKSTORE_SVC", "bookstore")
warehouseServiceName = utils.GetEnv("WAREHOUSE_SVC", "bookwarehouse")
bookstoreNamespace = utils.GetEnv(BookstoreNamespaceEnvVar, "bookstore")
bookwarehouseNamespace = utils.GetEnv(BookwarehouseNamespaceEnvVar, "bookwarehouse")
// Due to a limitation on kubernetes on Windows we need to use the FQDN
// otherwise DNS will not be able to resolve it.
// https://kubernetes.io/docs/setup/production-environment/windows/intro-windows-in-kubernetes/#dns-limitations
bookstoreService = fmt.Sprintf("%s.%s.svc.cluster.local:%d", bookstoreServiceName, bookstoreNamespace, bookstorePort)
warehouseService = fmt.Sprintf("%s.%s.svc.cluster.local:%d", warehouseServiceName, bookwarehouseNamespace, bookwarehousePort)
booksBought = fmt.Sprintf("http://%s/books-bought", bookstoreService)
buyBook = fmt.Sprintf("http://%s/buy-a-book/new", bookstoreService)
chargeAccountURL = fmt.Sprintf("http://%s/%s", warehouseService, RestockWarehouseURL)
interestingHeaders = []string{
IdentityHeader,
BooksBoughtHeader,
"Server",
"Date",
}
urlHeadersMap = map[string]map[string]string{
booksBought: {
"client-app": "bookbuyer", // this is a custom header
"user-agent": "Go-http-client/1.1",
},
buyBook: nil,
}
egressURLs = []string{
"edition.cnn.com",
"github.com",
}
)
var log = logger.NewPretty("demo")
// RestockBooks restocks the bookstore with certain amount of books from the warehouse.
func RestockBooks(amount int, headers map[string]string) {
log.Info().Msgf("Restocking from book warehouse with %d books", amount)
client := &http.Client{}
requestBody := strings.NewReader(strconv.Itoa(1))
req, err := http.NewRequest("POST", chargeAccountURL, requestBody)
req.Host = (fmt.Sprintf("%s.%s", warehouseServiceName, bookwarehouseNamespace))
for k, v := range headers {
req.Header.Set(k, v)
}
if err != nil {
log.Error().Err(err).Msgf("RestockBooks: error posting to %s", chargeAccountURL)
return
}
log.Info().Msgf("RestockBooks: Posted to %s with headers %v", req.URL, req.Header)
resp, err := client.Do(req)
if err != nil {
log.Error().Err(err).Msgf("RestockBooks: Error posting to %s", chargeAccountURL)
return
}
//nolint: errcheck
//#nosec G307
defer resp.Body.Close()
for _, hdr := range interestingHeaders {
log.Info().Msgf("RestockBooks (%s) adding header {%s: %s}", chargeAccountURL, hdr, getHeader(resp.Header, hdr))
}
log.Info().Msgf("RestockBooks (%s) finished w/ status: %s %d ", chargeAccountURL, resp.Status, resp.StatusCode)
}
// GetBooks reaches out to the bookstore and buys/steals books. This is invoked by the bookbuyer and the bookthief.
func GetBooks(participantName string, meshExpectedResponseCode int, booksCount *int64, booksCountV1 *int64, booksCountV2 *int64) {
minSuccessThreshold, maxIterations, sleepDurationBetweenRequests := getEnvVars(participantName)
// The URLs this participant will attempt to query from the bookstore service
urlSuccessMap := map[string]bool{
booksBought: false,
buyBook: false,
}
if enableEgress {
urlSuccessMap[httpPrefix] = false
urlSuccessMap[httpsPrefix] = false
}
urlExpectedRespCode := map[string]int{
booksBought: meshExpectedResponseCode,
buyBook: meshExpectedResponseCode,
// Using only prefixes as placeholders so that we can select random URL while testing
httpPrefix: getHTTPEgressExpectedResponseCode(),
httpsPrefix: getHTTPSEgressExpectedResponseCode(),
}
// Count how many times we have reached out to the bookstore
var iteration int64
// Count how many times BOTH urls have returned the expected status code
var successCount int64
// Keep state of the previous success/failure so we know when things regress
previouslySucceeded := false
for {
timedOut := maxIterations > 0 && iteration >= maxIterations
iteration++
fmt.Printf("\n\n--- %s:[ %d ] -----------------------------------------\n", participantName, iteration)
startTime := time.Now()
for url := range urlSuccessMap {
fetchURL := url
// Create random URLs to test egress
if fetchURL == httpPrefix || fetchURL == httpsPrefix {
index := rand.Intn(len(egressURLs)) // #nosec G404
fetchURL = fmt.Sprintf("%s%s", url, egressURLs[index])
}
// We only care about the response code of the HTTP(s) call for the given URL
responseCode, identity := fetch(fetchURL)
expectedResponseCode := urlExpectedRespCode[url]
succeeded := responseCode == expectedResponseCode
if !succeeded {
fmt.Printf("ERROR: response code for %q is %d; expected %d\n", url, responseCode, expectedResponseCode)
}
urlSuccessMap[url] = succeeded
// Regardless of what expect the response to be (depends on the policy) - in case of 200 OK - increase book counts.
if responseCode == http.StatusOK {
if url == buyBook {
if strings.HasPrefix(identity, "bookstore-v1") {
atomic.AddInt64(booksCountV1, 1)
atomic.AddInt64(booksCount, 1)
log.Info().Msgf("BooksCountV1=%d", booksCountV1)
} else if strings.HasPrefix(identity, "bookstore-v2") {
atomic.AddInt64(booksCountV2, 1)
atomic.AddInt64(booksCount, 1)
log.Info().Msgf("BooksCountV2=%d", booksCountV2)
}
}
}
// We are looking for a certain number of sequential successful HTTP requests.
if previouslySucceeded && allUrlsSucceeded(urlSuccessMap) {
successCount++
goalReached := successCount >= minSuccessThreshold
if goalReached && !timedOut {
// Sending this string to STDOUT will inform the CI Maestro that this is a succeeded;
// Maestro will stop tailing logs.
fmt.Println(Success)
}
}
if previouslySucceeded && !succeeded {
// This is a regression. We had success previously, but now we are seeing a failure.
// Reset the success counter.
successCount = 0
}
// Keep track of the previous state so we can track a) sequential successes and b) regressions.
previouslySucceeded = allUrlsSucceeded(urlSuccessMap)
}
if timedOut {
// We are over budget!
fmt.Printf("Threshold of %d iterations exceeded\n\n", maxIterations)
fmt.Print(Failure)
}
fillerTime := sleepDurationBetweenRequests - time.Since(startTime)
if fillerTime > 0 {
time.Sleep(fillerTime)
}
}
}
func allUrlsSucceeded(urlSucceeded map[string]bool) bool {
success := true
for _, succeeded := range urlSucceeded {
success = success && succeeded
}
return success
}
func fetch(url string) (responseCode int, identity string) {
headersMap := urlHeadersMap[url]
client := &http.Client{}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
fmt.Printf("Error requesting %s: %s\n", url, err)
}
for headerKey, headerValue := range headersMap {
req.Header.Add(headerKey, headerValue)
}
fmt.Printf("\nFetching %s\n", req.URL)
fmt.Printf("Request Headers: %v\n", req.Header)
resp, err := client.Do(req)
if err != nil {
fmt.Printf("Error fetching %s: %s\n", url, err)
} else {
//nolint: errcheck
//#nosec G307
defer resp.Body.Close()
responseCode = resp.StatusCode
for _, hdr := range interestingHeaders {
fmt.Printf("%s: %s\n", hdr, getHeader(resp.Header, hdr))
}
fmt.Printf("Status: %s\n", resp.Status)
}
identity = "unknown"
if resp != nil && resp.Header != nil {
identity = getHeader(resp.Header, IdentityHeader)
}
return responseCode, identity
}
func getHeader(headers map[string][]string, header string) string {
val, ok := headers[header]
if !ok {
val = []string{"n/a"}
}
return strings.Join(val, ", ")
}
func getEnvVars(participantName string) (minSuccessThreshold int64, maxIterations int64, sleepDurationBetweenRequests time.Duration) {
log := logger.New(fmt.Sprintf("demo/%s", participantName))
var err error
minSuccessThreshold, err = strconv.ParseInt(minSuccessThresholdStr, 10, 32)
if err != nil {
log.Fatal().Err(err).Msgf("Error parsing integer environment variable %q", minSuccessThresholdStr)
}
maxIterations, err = strconv.ParseInt(maxIterationsStr, 10, 32)
if err != nil {
log.Fatal().Err(err).Msgf("Error parsing integer environment variable %q", maxIterationsStr)
}
sleepDurationBetweenRequestsInt, err := strconv.ParseInt(sleepDurationBetweenRequestsSecondsStr, 10, 32)
if err != nil {
log.Fatal().Err(err).Msgf("Error parsing integer environment variable %q", sleepDurationBetweenRequestsSecondsStr)
}
return minSuccessThreshold, maxIterations, time.Duration(sleepDurationBetweenRequestsInt) * time.Second
}
// GetExpectedResponseCodeFromEnvVar returns the expected response code based on the given environment variable
func GetExpectedResponseCodeFromEnvVar(envVar, defaultValue string) int {
expectedRespCodeStr := utils.GetEnv(envVar, defaultValue)
expectedRespCode, err := strconv.ParseInt(expectedRespCodeStr, 10, 0)
if err != nil {
log.Fatal().Err(err).Msgf("Could not convert environment variable %s='%s' to int", envVar, expectedRespCodeStr)
}
return int(expectedRespCode)
}
// getHTTPSEgressExpectedResponseCode returns the expected response code for HTTPS egress.
// Since HTTPS egress depends on clients to originate TLS, when egress is disabled the
// TLS negotiation will fail. As a result no HTTP response code will be returned
// but rather the HTTP library will return 0 as the status code in such cases.
func getHTTPSEgressExpectedResponseCode() int {
if enableEgress {
return http.StatusOK
}
return 0
}
// getHTTPEgressExpectedResponseCode returns the expected response code for HTTP egress
func getHTTPEgressExpectedResponseCode() int {
if enableEgress {
return http.StatusOK
}
return http.StatusNotFound
}
// GetRawGenerator returns a function that can be used to write a response of book data
func GetRawGenerator(books interface{}) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
booksRaw, err := json.Marshal(books)
if err != nil {
log.Fatal().Err(err).Msg("Failed to marshal book data")
}
_, err = w.Write(booksRaw)
if err != nil {
log.Fatal().Err(err).Msg("Failed to write raw output")
}
}
}
// GetTracingHeaderKeys returns header keys used for distributed tracing with Jaeger
func GetTracingHeaderKeys() []string {
return []string{"X-Ot-Span-Context", "X-Request-Id", "uber-trace-id", "x-b3-traceid", "x-b3-spanid", "x-b3-parentspanid"}
}
// GetTracingHeaders gets the tracing related header values from a request
func GetTracingHeaders(r *http.Request) map[string]string {
var headers = map[string]string{}
for _, key := range GetTracingHeaderKeys() {
if v := r.Header.Get(key); v != "" {
headers[key] = v
}
}
return headers
}